diff --git a/.bazeliskversion b/.bazeliskversion index 661e7aeadf36f..6a126f402d53d 100644 --- a/.bazeliskversion +++ b/.bazeliskversion @@ -1 +1 @@ -1.7.3 +1.7.5 diff --git a/docs/discover/images/discover-search-for-relevance.png b/docs/discover/images/discover-search-for-relevance.png new file mode 100644 index 0000000000000..4d59ad186ded4 Binary files /dev/null and b/docs/discover/images/discover-search-for-relevance.png differ diff --git a/docs/discover/search-for-relevance.asciidoc b/docs/discover/search-for-relevance.asciidoc new file mode 100644 index 0000000000000..0232c02b6e061 --- /dev/null +++ b/docs/discover/search-for-relevance.asciidoc @@ -0,0 +1,24 @@ +[[discover-search-for-relevance]] +== Search for relevance +Sometimes you might be unsure which documents best match your question. +{es} assigns a relevancy, or score to each document, so you can +can narrow your search to the documents with the most relevant results. +The higher the score, the better it matches your query. + +For example, suppose you have the <>, and you're a searching for +a flight that arrived or departed from `Warsaw` or `Venice` when the weather was clear. + +. In *Discover*, open the index pattern dropdown, and select `kibana_sample_data_flight`. +. In the query bar, click *KQL*, and switch to the <>. +. Search for `Warsaw OR Venice OR Clear`. +. If you don't see any results, open the time filter and select a time range that contains data. +. From the list of *Available fields*, add `_score` to the document table. +. In the document table, click the header for the `_score` column, and then sort the column by descending scores. ++ +The results are currently sorted by first `Time`, and then by `_score`. +. To sort only by `_score`, remove the `Time` field. ++ +Your table now shows documents with the best matches, from most to least relevant. ++ +[role="screenshot"] +image::images/discover-search-for-relevance.png["Example of a search for relevance"] diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index c3185aaad553a..cadf8e0b16a44 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -105,7 +105,7 @@ See <> for how to obtain the endpoint and + * The action’s type: Trigger, Resolve, or Acknowledge. * The event’s severity: Info, warning, error, or critical. -* An array of different fields, including the timestamp, group, class, component, and your dedup key. +* An array of different fields, including the timestamp, group, class, component, and your dedup key. By default, the dedup is configured to create a new PagerDuty incident for each alert instance and reuse the incident when a recovered alert instance reactivates. Depending on your custom needs, assign them variables from the alerting context. To see the available context variables, click on the *Add alert variable* icon next to each corresponding field. For more details on these parameters, see the @@ -179,7 +179,7 @@ PagerDuty actions have the following properties: Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 993d815c37f71..5983804c5c862 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -30,6 +30,14 @@ For domain-specific alerts, refer to the documentation for that app. * <> * <> +[NOTE] +============================================== +Some alert types are subscription features, while others are free features. +For a comparison of the Elastic subscription levels, +see {subscriptions}[the subscription page]. +============================================== + + include::stack-alerts/index-threshold.asciidoc[] include::stack-alerts/es-query.asciidoc[] include::maps-alerts/geo-alert-types.asciidoc[] diff --git a/docs/user/discover.asciidoc b/docs/user/discover.asciidoc index a08713421a7fb..42ac1e22ce167 100644 --- a/docs/user/discover.asciidoc +++ b/docs/user/discover.asciidoc @@ -198,7 +198,6 @@ image:images/visualize-from-discover.png[Visualization that opens from Discover [float] === What’s next? - * <>. * <> to better meet your needs. @@ -209,6 +208,8 @@ the table columns that display by default, and more. * <>. +* <>. + -- include::{kib-repo-dir}/management/index-patterns.asciidoc[] @@ -216,3 +217,5 @@ include::{kib-repo-dir}/management/index-patterns.asciidoc[] include::{kib-repo-dir}/discover/set-time-filter.asciidoc[] include::{kib-repo-dir}/discover/search.asciidoc[] + +include::{kib-repo-dir}/discover/search-for-relevance.asciidoc[] diff --git a/package.json b/package.json index 90096bfdf1b80..3dde0c6a17fb5 100644 --- a/package.json +++ b/package.json @@ -655,6 +655,7 @@ "fetch-mock": "^7.3.9", "file-loader": "^4.2.0", "file-saver": "^1.3.8", + "form-data": "^4.0.0", "formsy-react": "^1.1.5", "geckodriver": "^1.21.0", "glob-watcher": "5.0.3", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 66ad4e7be589b..3ac3927d25c05 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -23,7 +23,6 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './certs'; -export * from './kbn_client'; export * from './run'; export * from './axios'; export * from './stdio'; diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts index 3ee6676cf5e32..2d1cd7b9f97ca 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts @@ -50,7 +50,6 @@ export function buildApiDeclaration( name?: string ): ApiDeclaration { const apiName = name ? name : isNamedNode(node) ? node.getName() : 'Unnamed'; - log.debug(`Building API Declaration for ${apiName} of kind ${node.getKindName()}`); const apiId = parentApiId ? parentApiId + '.' + apiName : apiName; const anchorLink: AnchorLink = { scope, pluginName, apiName: apiId }; diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts index 146fcf4fa4d0a..2f041c8d42b4b 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_arrow_fn_dec.ts @@ -47,9 +47,6 @@ export function getArrowFunctionDec( anchorLink: AnchorLink, log: ToolingLog ) { - log.debug( - `Getting Arrow Function doc def for node ${node.getName()} of kind ${node.getKindName()}` - ); return { id: getApiSectionId(anchorLink), type: TypeKind.FunctionKind, diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts index 2936699152a83..89050430085fd 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts @@ -39,7 +39,6 @@ export function buildFunctionDec( const label = Node.isConstructorDeclaration(node) ? 'Constructor' : node.getName() || '(WARN: Missing name)'; - log.debug(`Getting function doc def for node ${label} of kind ${node.getKindName()}`); return { id: getApiSectionId(anchorLink), type: TypeKind.FunctionKind, diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts index 3e0b48de1e18b..86e8e5078b6fb 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_variable_dec.ts @@ -45,7 +45,6 @@ export function buildVariableDec( anchorLink: AnchorLink, log: ToolingLog ): ApiDeclaration { - log.debug('buildVariableDec for ' + node.getName()); const initializer = node.getInitializer(); // Recusively list object properties as children. if (initializer && Node.isObjectLiteralExpression(initializer)) { diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts index a757df2ece366..f3fafca4c1e82 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { REPO_ROOT } from '@kbn/utils'; import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; import { getPluginApiDocId } from '../utils'; import { extractImportReferences } from './extract_import_refs'; @@ -82,7 +83,43 @@ it('test extractImportReference with unknown imports', () => { expect(results.length).toBe(3); expect(results[0]).toBe(''); +}); + +it('test full file imports with no matching plugin', () => { + const refs = extractImportReferences( + `typeof import("${REPO_ROOT}/src/plugins/data/common/es_query/kuery/node_types/function")`, + plugins, + log + ); + expect(refs).toMatchInlineSnapshot(` + Array [ + "typeof ", + "src/plugins/data/common/es_query/kuery/node_types/function", + ] + `); + expect(refs.length).toBe(2); +}); + +it('test full file imports with a matching plugin', () => { + const refs = extractImportReferences( + `typeof import("${plugin.directory}/public/foo/index") something`, + plugins, + log + ); + expect(refs).toMatchInlineSnapshot(` + Array [ + "typeof ", + Object { + "docId": "kibPluginAPluginApi", + "pluginId": "pluginA", + "scope": "public", + "section": undefined, + "text": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index", + }, + " something", + ] + `); + expect(refs.length).toBe(3); }); it('test single link', () => { diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts index 1147e15a1acb6..92f191197472d 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/extract_import_refs.ts @@ -9,6 +9,7 @@ import { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; import { getApiSectionId, getPluginApiDocId, getPluginForPath } from '../utils'; import { ApiScope, TextWithLinks } from '../types'; +import { getRelativePath } from './utils'; /** * @@ -54,6 +55,9 @@ export function extractImportReferences( const str = textSegment.substr(index + length - name.length, name.length); if (str && str !== '') { texts.push(str); + } else { + // If there is no ".Name" then use the full path. You can see things like "typeof import("file")" + texts.push(getRelativePath(path)); } } else { const section = getApiSectionId({ @@ -69,10 +73,12 @@ export function extractImportReferences( apiPath: path, directory: plugin.directory, }), - section, - text: name, + section: name && name !== '' ? section : undefined, + text: name && name !== '' ? name : getRelativePath(path), }); } + + // Prep textSegment to skip past the `import`, then check for more. textSegment = textSegment.substr(index + length); } else { if (textSegment && textSegment !== '') { @@ -87,10 +93,10 @@ export function extractImportReferences( function extractImportRef( str: string ): { path: string; name: string; index: number; length: number } | undefined { - const groups = str.match(/import\("(.*?)"\)\.(\w*)/); + const groups = str.match(/import\("(.*?)"\)\.?(\w*)/); if (groups) { const path = groups[1]; - const name = groups[2]; + const name = groups.length > 2 ? groups[2] : ''; const index = groups.index!; const length = groups[0].length; return { path, name, index, length }; diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts index 9efa96b6e9676..76683d968b9c2 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts @@ -17,7 +17,7 @@ export function isPrivate(node: ParameterDeclaration | ClassMemberTypes): boolea /** * Change the absolute path into a relative one. */ -function getRelativePath(fullPath: string): string { +export function getRelativePath(fullPath: string): string { return Path.relative(REPO_ROOT, fullPath); } diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts index b35515eb9d209..608c30f068357 100644 --- a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts +++ b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_mdx_docs.ts @@ -84,7 +84,7 @@ import ${json} from './${fileName}.json'; common: groupPluginApi(doc.common), server: groupPluginApi(doc.server), }; - fs.writeFileSync(Path.resolve(folder, fileName + '.json'), JSON.stringify(scopedDoc)); + fs.writeFileSync(Path.resolve(folder, fileName + '.json'), JSON.stringify(scopedDoc, null, 2)); mdx += scopApiToMdx(scopedDoc.client, 'Client', json, 'client'); mdx += scopApiToMdx(scopedDoc.server, 'Server', json, 'server'); diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json index db25b8c4f021e..ab605006d7e3d 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.json @@ -1 +1,1566 @@ -{"id":"pluginA","client":{"classes":[{"id":"def-public.ExampleClass","type":"Class","label":"ExampleClass","description":[],"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleClass","text":"ExampleClass"}," implements ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"children":[{"id":"def-public.ExampleClass.component","type":"CompoundType","label":"component","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":30,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L30"},"signature":["React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined"]},{"id":"def-public.ExampleClass.Unnamed","type":"Function","label":"Constructor","signature":["any"],"description":[],"children":[{"type":"Uncategorized","label":"t","isRequired":true,"signature":["T"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":32,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32"}}],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":32,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32"}},{"id":"def-public.ExampleClass.arrowFn","type":"Function","children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40"}}],"signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["\nan arrow fn on a class."],"label":"arrowFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40"},"returnComment":[]},{"id":"def-public.ExampleClass.getVar","type":"Function","label":"getVar","signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => string"],"description":["\nA function on a class."],"children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["a param"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46"}}],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":24,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L24"},"initialIsOpen":false},{"id":"def-public.CrazyClass","type":"Class","label":"CrazyClass","description":[],"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.CrazyClass","text":"CrazyClass"},"

extends ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleClass","text":"ExampleClass"},"<",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.WithGen","text":"WithGen"},"

>"],"children":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":51,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L51"},"initialIsOpen":false}],"functions":[{"id":"def-public.notAnArrowFn","type":"Function","label":"notAnArrowFn","signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["\nThis is a non arrow function.\n"],"children":[{"type":"string","label":"a","isRequired":true,"signature":["string"],"description":["The letter A"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":22,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L22"}},{"type":"number","label":"b","isRequired":false,"signature":["number | undefined"],"description":["Feed me to the function"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":23,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L23"}},{"type":"Array","label":"c","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["So many params"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":24,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L24"}},{"type":"CompoundType","label":"d","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["a great param"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":25,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L25"}},{"type":"string","label":"e","isRequired":false,"signature":["string | undefined"],"description":["Another comment"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L26"}}],"tags":[],"returnComment":["something!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L21"},"initialIsOpen":false},{"id":"def-public.arrowFn","type":"Function","children":[{"type":"string","label":"a","isRequired":true,"signature":["string"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":42,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L42"}},{"type":"number","label":"b","isRequired":false,"signature":["number | undefined"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L43"}},{"type":"Array","label":"c","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L44"}},{"type":"CompoundType","label":"d","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":45,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L45"}},{"type":"string","label":"e","isRequired":false,"signature":["string | undefined"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":46,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L46"}}],"signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e?: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"description":["\nThis is an arrow function.\n"],"label":"arrowFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":41,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L41"},"returnComment":["something!"],"initialIsOpen":false},{"id":"def-public.crazyFunction","type":"Function","children":[{"id":"def-public.crazyFunction.obj","type":"Object","label":"obj","description":[],"children":[{"id":"def-public.crazyFunction.obj.hi","type":"string","label":"hi","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67"}},{"id":"def-public.crazyFunction.{-fn }","type":"Object","label":"{ fn }","description":[],"children":[{"id":"def-public.crazyFunction.{-fn }.fn","type":"Function","label":"fn","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68"},"signature":["(foo: { param: string; }) => number"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68"}},{"id":"def-public.crazyFunction.{-str }","type":"Object","label":"{ str }","description":[],"children":[{"id":"def-public.crazyFunction.{-str }.str","type":"string","label":"str","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":69,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":69,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69"}}],"signature":["(obj: { hi: string; }, { fn }: { fn: (foo: { param: string; }) => number; }, { str }: { str: string; }) => () => () => number"],"description":["\nWho would write such a complicated function?? Ewwww.\n\nAccording to https://jsdoc.app/tags-param.html#parameters-with-properties,\nthis is how destructured arguements should be commented.\n"],"label":"crazyFunction","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":66,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L66"},"returnComment":["I have no idea."],"initialIsOpen":false},{"id":"def-public.fnWithNonExportedRef","type":"Function","children":[{"type":"Object","label":"a","isRequired":true,"signature":["ImNotExported"],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76"}}],"signature":["(a: ImNotExported) => string"],"description":[],"label":"fnWithNonExportedRef","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76"},"returnComment":[],"initialIsOpen":false}],"interfaces":[{"id":"def-public.SearchSpec","type":"Interface","label":"SearchSpec","description":["\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password."],"children":[{"id":"def-public.SearchSpec.username","type":"string","label":"username","description":["\nStores the username. Duh,"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L26"}},{"id":"def-public.SearchSpec.password","type":"string","label":"password","description":["\nStores the password. I hope it's encrypted!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":30,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L30"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":22,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L22"},"initialIsOpen":false},{"id":"def-public.WithGen","type":"Interface","label":"WithGen","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.WithGen","text":"WithGen"},""],"description":["\nAn interface with a generic."],"children":[{"id":"def-public.WithGen.t","type":"Uncategorized","label":"t","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L17"},"signature":["T"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":16,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L16"},"initialIsOpen":false},{"id":"def-public.AnotherInterface","type":"Interface","label":"AnotherInterface","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"description":[],"children":[{"id":"def-public.AnotherInterface.t","type":"Uncategorized","label":"t","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L21"},"signature":["T"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":20,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L20"},"initialIsOpen":false},{"id":"def-public.ExampleInterface","type":"Interface","label":"ExampleInterface","signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ExampleInterface","text":"ExampleInterface"}," extends ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.AnotherInterface","text":"AnotherInterface"},""],"description":["\nThis is an example interface so we can see how it appears inside the API\ndocumentation system."],"children":[{"id":"def-public.ExampleInterface.getAPromiseThatResolvesToString","type":"Function","label":"getAPromiseThatResolvesToString","description":["\nThis gets a promise that resolves to a string."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":61,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L61"},"signature":["() => Promise"]},{"id":"def-public.ExampleInterface.aFnWithGen","type":"Function","label":"aFnWithGen","description":["\nThis function takes a generic. It was sometimes being tripped on\nand returned as an unknown type with no signature."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":67,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L67"},"signature":["(t: T) => void"]},{"id":"def-public.ExampleInterface.aFn","type":"Function","label":"aFn","signature":["() => void"],"description":["\nThese are not coming back properly."],"children":[],"tags":[],"returnComment":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":72,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L72"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":57,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L57"},"initialIsOpen":false},{"id":"def-public.IReturnAReactComponent","type":"Interface","label":"IReturnAReactComponent","description":["\nAn interface that has a react component."],"children":[{"id":"def-public.IReturnAReactComponent.component","type":"CompoundType","label":"component","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":79,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L79"},"signature":["React.ComponentType<{}>"]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts","lineNumber":78,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L78"},"initialIsOpen":false},{"id":"def-public.ImAnObject","type":"Interface","label":"ImAnObject","description":[],"children":[{"id":"def-public.ImAnObject.foo","type":"Function","label":"foo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L44"},"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.FnWithGeneric","text":"FnWithGeneric"}]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L43"},"initialIsOpen":false}],"enums":[{"id":"def-public.DayOfWeek","type":"Enum","label":"DayOfWeek","description":["\nComments on enums."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L31"},"initialIsOpen":false}],"misc":[{"id":"def-public.imAnAny","type":"Any","label":"imAnAny","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts","lineNumber":19,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L19"},"signature":["any"],"initialIsOpen":false},{"id":"def-public.imAnUnknown","type":"Unknown","label":"imAnUnknown","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts","lineNumber":20,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L20"},"signature":["unknown"],"initialIsOpen":false},{"id":"def-public.NotAnArrowFnType","type":"Type","label":"NotAnArrowFnType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts","lineNumber":78,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L78"},"signature":["(a: string, b: number | undefined, c: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},", d: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},", e: string | undefined) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"initialIsOpen":false},{"id":"def-public.aUnionProperty","type":"CompoundType","label":"aUnionProperty","description":["\nThis is a complicated union type"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":51,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L51"},"signature":["string | number | (() => string) | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.CrazyClass","text":"CrazyClass"},""],"initialIsOpen":false},{"id":"def-public.aStrArray","type":"Array","label":"aStrArray","description":["\nThis is an array of strings. The type is explicit."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":56,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L56"},"signature":["string[]"],"initialIsOpen":false},{"id":"def-public.aNumArray","type":"Array","label":"aNumArray","description":["\nThis is an array of numbers. The type is implied."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":61,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L61"},"signature":["number[]"],"initialIsOpen":false},{"id":"def-public.aStr","type":"string","label":"aStr","description":["\nA string that says hi to you!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":66,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L66"},"initialIsOpen":false},{"id":"def-public.aNum","type":"number","label":"aNum","description":["\nIt's a number. A special number."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":71,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L71"},"signature":["10"],"initialIsOpen":false},{"id":"def-public.literalString","type":"string","label":"literalString","description":["\nI'm a type of string, but more specifically, a literal string type."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":76,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L76"},"signature":["\"HI\""],"initialIsOpen":false},{"id":"def-public.StringOrUndefinedType","type":"Type","label":"StringOrUndefinedType","description":["\nHow should a potentially undefined type show up."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":15,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L15"},"signature":["undefined | string"],"initialIsOpen":false},{"id":"def-public.TypeWithGeneric","type":"Type","label":"TypeWithGeneric","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L17"},"signature":["T[]"],"initialIsOpen":false},{"id":"def-public.ImAType","type":"Type","label":"ImAType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":19,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L19"},"signature":["string | number | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAFooPluginApi","section":"def-public.FooType","text":"FooType"}," | ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"}," | ",{"pluginId":"pluginA","scope":"common","docId":"kibPluginAPluginApi","section":"def-common.ImACommonType","text":"ImACommonType"}],"initialIsOpen":false},{"id":"def-public.FnWithGeneric","type":"Type","label":"FnWithGeneric","description":["\nThis is a type that defines a function.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L26"},"signature":["(t: T) => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.TypeWithGeneric","text":"TypeWithGeneric"},""],"initialIsOpen":false},{"id":"def-public.MultipleDeclarationsType","type":"Type","label":"MultipleDeclarationsType","description":["\nCalling node.getSymbol().getDeclarations() will return > 1 declaration."],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":40,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L40"},"signature":["(typeof DayOfWeek)[]"],"initialIsOpen":false},{"id":"def-public.IRefANotExportedType","type":"Type","label":"IRefANotExportedType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts","lineNumber":42,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L42"},"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAFooPluginApi","section":"def-public.ImNotExportedFromIndex","text":"ImNotExportedFromIndex"}," | { zed: \"hi\"; }"],"initialIsOpen":false}],"objects":[{"id":"def-public.aPretendNamespaceObj","type":"Object","children":[{"id":"def-public.aPretendNamespaceObj.notAnArrowFn","type":"Function","label":"notAnArrowFn","description":["/**\n * The docs should show this inline comment.\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":21,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L21"},"signature":["typeof ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.notAnArrowFn","text":"notAnArrowFn"}]},{"id":"def-public.aPretendNamespaceObj.aPropertyMisdirection","type":"Function","label":"aPropertyMisdirection","description":["/**\n * Should this comment show up?\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":26,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L26"},"signature":["typeof ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.notAnArrowFn","text":"notAnArrowFn"}]},{"id":"def-public.aPretendNamespaceObj.aPropertyInlineFn","type":"Function","children":[{"type":"CompoundType","label":"a","isRequired":true,"signature":[{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31"}}],"signature":["(a: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"},") => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.ImAType","text":"ImAType"}],"description":["/**\n * I'm a property inline fun.\n */"],"label":"aPropertyInlineFn","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":31,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31"},"returnComment":[]},{"id":"def-public.aPretendNamespaceObj.aPropertyStr","type":"string","label":"aPropertyStr","description":["/**\n * The only way for this to have a comment is to grab this.\n */"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":38,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L38"}},{"id":"def-public.aPretendNamespaceObj.nestedObj","type":"Object","children":[{"id":"def-public.aPretendNamespaceObj.nestedObj.foo","type":"string","label":"foo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":44,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L44"}}],"description":["/**\n * Will this nested object have it's children extracted appropriately?\n */"],"label":"nestedObj","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":43,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L43"}}],"description":["\nSome of the plugins wrap static exports in an object to create\na namespace like this."],"label":"aPretendNamespaceObj","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts","lineNumber":17,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L17"},"initialIsOpen":false}],"setup":{"id":"def-public.Setup","type":"Interface","label":"Setup","description":["\nAccess setup functionality from your plugin's setup function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n setup(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchService();\n }\n}\n```"],"children":[{"id":"def-public.Setup.getSearchService","type":"Function","label":"getSearchService","description":["\nA factory function that returns a new instance of Foo based\non the spec. We aren't sure if this is a good function so it's marked\nbeta. That should be clear in the docs because of the js doc tag.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":96,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L96"},"signature":["(searchSpec: ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.SearchSpec","text":"SearchSpec"},") => string"]},{"id":"def-public.Setup.getSearchService2","type":"Function","label":"getSearchService2","description":["\nThis uses an inlined object type rather than referencing an exported type, which is discouraged.\nprefer the way {@link getSearchService} is typed.\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":104,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L104"},"signature":["(searchSpec: { username: string; password: string; }) => string"]},{"id":"def-public.Setup.doTheThing","type":"Function","label":"doTheThing","description":["\nThis function does the thing and it's so good at it! But we decided to deprecate it\nanyway. I hope that's clear to developers in the docs!\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":117,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L117"},"signature":["(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void"]},{"id":"def-public.Setup.fnWithInlineParams","type":"Function","label":"fnWithInlineParams","description":["\nWho would write such a complicated function?? Ew, how will the obj parameter appear in docs?\n"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":128,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L128"},"signature":["(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }"]},{"id":"def-public.Setup.id","type":"string","label":"id","description":["\nHi, I'm a comment for an id string!"],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":135,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L135"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":84,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L84"},"lifecycle":"setup","initialIsOpen":true},"start":{"id":"def-public.Start","type":"Interface","label":"Start","description":["\nAccess start functionality from your plugin's start function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n start(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchLanguage();\n }\n}\n```"],"children":[{"id":"def-public.Start.getSearchLanguage","type":"Function","label":"getSearchLanguage","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":68,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L68"},"signature":["() => ",{"pluginId":"pluginA","scope":"public","docId":"kibPluginAPluginApi","section":"def-public.SearchLanguage","text":"SearchLanguage"}]}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts","lineNumber":64,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L64"},"lifecycle":"start","initialIsOpen":true}},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[{"id":"def-common.ImACommonType","type":"Interface","label":"ImACommonType","description":[],"children":[{"id":"def-common.ImACommonType.goo","type":"number","label":"goo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts","lineNumber":12,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L12"}}],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L11"},"initialIsOpen":false}],"enums":[],"misc":[],"objects":[]}} \ No newline at end of file +{ + "id": "pluginA", + "client": { + "classes": [ + { + "id": "def-public.ExampleClass", + "type": "Class", + "label": "ExampleClass", + "description": [], + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleClass", + "text": "ExampleClass" + }, + " implements ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface" + }, + "" + ], + "children": [ + { + "id": "def-public.ExampleClass.component", + "type": "CompoundType", + "label": "component", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 30, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L30" + }, + "signature": [ + "React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined" + ] + }, + { + "id": "def-public.ExampleClass.Unnamed", + "type": "Function", + "label": "Constructor", + "signature": [ + "any" + ], + "description": [], + "children": [ + { + "type": "Uncategorized", + "label": "t", + "isRequired": true, + "signature": [ + "T" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 32, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32" + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 32, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L32" + } + }, + { + "id": "def-public.ExampleClass.arrowFn", + "type": "Function", + "children": [ + { + "type": "CompoundType", + "label": "a", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 40, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40" + } + } + ], + "signature": [ + "(a: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ") => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "\nan arrow fn on a class." + ], + "label": "arrowFn", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 40, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L40" + }, + "returnComment": [] + }, + { + "id": "def-public.ExampleClass.getVar", + "type": "Function", + "label": "getVar", + "signature": [ + "(a: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ") => string" + ], + "description": [ + "\nA function on a class." + ], + "children": [ + { + "type": "CompoundType", + "label": "a", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "a param" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 46, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46" + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 46, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L46" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 24, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L24" + }, + "initialIsOpen": false + }, + { + "id": "def-public.CrazyClass", + "type": "Class", + "label": "CrazyClass", + "description": [], + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.CrazyClass", + "text": "CrazyClass" + }, + "

extends ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleClass", + "text": "ExampleClass" + }, + "<", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.WithGen", + "text": "WithGen" + }, + "

>" + ], + "children": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 51, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L51" + }, + "initialIsOpen": false + } + ], + "functions": [ + { + "id": "def-public.notAnArrowFn", + "type": "Function", + "label": "notAnArrowFn", + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "\nThis is a non arrow function.\n" + ], + "children": [ + { + "type": "string", + "label": "a", + "isRequired": true, + "signature": [ + "string" + ], + "description": [ + "The letter A" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 22, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L22" + } + }, + { + "type": "number", + "label": "b", + "isRequired": false, + "signature": [ + "number | undefined" + ], + "description": [ + "Feed me to the function" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 23, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L23" + } + }, + { + "type": "Array", + "label": "c", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "So many params" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 24, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L24" + } + }, + { + "type": "CompoundType", + "label": "d", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "a great param" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 25, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L25" + } + }, + { + "type": "string", + "label": "e", + "isRequired": false, + "signature": [ + "string | undefined" + ], + "description": [ + "Another comment" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L26" + } + } + ], + "tags": [], + "returnComment": [ + "something!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 21, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L21" + }, + "initialIsOpen": false + }, + { + "id": "def-public.arrowFn", + "type": "Function", + "children": [ + { + "type": "string", + "label": "a", + "isRequired": true, + "signature": [ + "string" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 42, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L42" + } + }, + { + "type": "number", + "label": "b", + "isRequired": false, + "signature": [ + "number | undefined" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 43, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L43" + } + }, + { + "type": "Array", + "label": "c", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 44, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L44" + } + }, + { + "type": "CompoundType", + "label": "d", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 45, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L45" + } + }, + { + "type": "string", + "label": "e", + "isRequired": false, + "signature": [ + "string | undefined" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 46, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L46" + } + } + ], + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e?: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "description": [ + "\nThis is an arrow function.\n" + ], + "label": "arrowFn", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 41, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L41" + }, + "returnComment": [ + "something!" + ], + "initialIsOpen": false + }, + { + "id": "def-public.crazyFunction", + "type": "Function", + "children": [ + { + "id": "def-public.crazyFunction.obj", + "type": "Object", + "label": "obj", + "description": [], + "children": [ + { + "id": "def-public.crazyFunction.obj.hi", + "type": "string", + "label": "hi", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 67, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L67" + } + }, + { + "id": "def-public.crazyFunction.{-fn }", + "type": "Object", + "label": "{ fn }", + "description": [], + "children": [ + { + "id": "def-public.crazyFunction.{-fn }.fn", + "type": "Function", + "label": "fn", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 68, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68" + }, + "signature": [ + "(foo: { param: string; }) => number" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 68, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L68" + } + }, + { + "id": "def-public.crazyFunction.{-str }", + "type": "Object", + "label": "{ str }", + "description": [], + "children": [ + { + "id": "def-public.crazyFunction.{-str }.str", + "type": "string", + "label": "str", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 69, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 69, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L69" + } + } + ], + "signature": [ + "(obj: { hi: string; }, { fn }: { fn: (foo: { param: string; }) => number; }, { str }: { str: string; }) => () => () => number" + ], + "description": [ + "\nWho would write such a complicated function?? Ewwww.\n\nAccording to https://jsdoc.app/tags-param.html#parameters-with-properties,\nthis is how destructured arguements should be commented.\n" + ], + "label": "crazyFunction", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 66, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L66" + }, + "returnComment": [ + "I have no idea." + ], + "initialIsOpen": false + }, + { + "id": "def-public.fnWithNonExportedRef", + "type": "Function", + "children": [ + { + "type": "Object", + "label": "a", + "isRequired": true, + "signature": [ + "ImNotExported" + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 76, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76" + } + } + ], + "signature": [ + "(a: ImNotExported) => string" + ], + "description": [], + "label": "fnWithNonExportedRef", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 76, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L76" + }, + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "id": "def-public.SearchSpec", + "type": "Interface", + "label": "SearchSpec", + "description": [ + "\nThe SearchSpec interface contains settings for creating a new SearchService, like\nusername and password." + ], + "children": [ + { + "id": "def-public.SearchSpec.username", + "type": "string", + "label": "username", + "description": [ + "\nStores the username. Duh," + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L26" + } + }, + { + "id": "def-public.SearchSpec.password", + "type": "string", + "label": "password", + "description": [ + "\nStores the password. I hope it's encrypted!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 30, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L30" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 22, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L22" + }, + "initialIsOpen": false + }, + { + "id": "def-public.WithGen", + "type": "Interface", + "label": "WithGen", + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.WithGen", + "text": "WithGen" + }, + "" + ], + "description": [ + "\nAn interface with a generic." + ], + "children": [ + { + "id": "def-public.WithGen.t", + "type": "Uncategorized", + "label": "t", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 17, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L17" + }, + "signature": [ + "T" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 16, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L16" + }, + "initialIsOpen": false + }, + { + "id": "def-public.AnotherInterface", + "type": "Interface", + "label": "AnotherInterface", + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface" + }, + "" + ], + "description": [], + "children": [ + { + "id": "def-public.AnotherInterface.t", + "type": "Uncategorized", + "label": "t", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 21, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L21" + }, + "signature": [ + "T" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 20, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L20" + }, + "initialIsOpen": false + }, + { + "id": "def-public.ExampleInterface", + "type": "Interface", + "label": "ExampleInterface", + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ExampleInterface", + "text": "ExampleInterface" + }, + " extends ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.AnotherInterface", + "text": "AnotherInterface" + }, + "" + ], + "description": [ + "\nThis is an example interface so we can see how it appears inside the API\ndocumentation system." + ], + "children": [ + { + "id": "def-public.ExampleInterface.getAPromiseThatResolvesToString", + "type": "Function", + "label": "getAPromiseThatResolvesToString", + "description": [ + "\nThis gets a promise that resolves to a string." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 61, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L61" + }, + "signature": [ + "() => Promise" + ] + }, + { + "id": "def-public.ExampleInterface.aFnWithGen", + "type": "Function", + "label": "aFnWithGen", + "description": [ + "\nThis function takes a generic. It was sometimes being tripped on\nand returned as an unknown type with no signature." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 67, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L67" + }, + "signature": [ + "(t: T) => void" + ] + }, + { + "id": "def-public.ExampleInterface.aFn", + "type": "Function", + "label": "aFn", + "signature": [ + "() => void" + ], + "description": [ + "\nThese are not coming back properly." + ], + "children": [], + "tags": [], + "returnComment": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 72, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L72" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 57, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L57" + }, + "initialIsOpen": false + }, + { + "id": "def-public.IReturnAReactComponent", + "type": "Interface", + "label": "IReturnAReactComponent", + "description": [ + "\nAn interface that has a react component." + ], + "children": [ + { + "id": "def-public.IReturnAReactComponent.component", + "type": "CompoundType", + "label": "component", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 79, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L79" + }, + "signature": [ + "React.ComponentType<{}>" + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts", + "lineNumber": 78, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/classes.ts#L78" + }, + "initialIsOpen": false + }, + { + "id": "def-public.ImAnObject", + "type": "Interface", + "label": "ImAnObject", + "description": [], + "children": [ + { + "id": "def-public.ImAnObject.foo", + "type": "Function", + "label": "foo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 44, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L44" + }, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.FnWithGeneric", + "text": "FnWithGeneric" + } + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 43, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L43" + }, + "initialIsOpen": false + } + ], + "enums": [ + { + "id": "def-public.DayOfWeek", + "type": "Enum", + "label": "DayOfWeek", + "description": [ + "\nComments on enums." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 31, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L31" + }, + "initialIsOpen": false + } + ], + "misc": [ + { + "id": "def-public.imAnAny", + "type": "Any", + "label": "imAnAny", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 19, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L19" + }, + "signature": [ + "any" + ], + "initialIsOpen": false + }, + { + "id": "def-public.imAnUnknown", + "type": "Unknown", + "label": "imAnUnknown", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "lineNumber": 20, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts#L20" + }, + "signature": [ + "unknown" + ], + "initialIsOpen": false + }, + { + "id": "def-public.NotAnArrowFnType", + "type": "Type", + "label": "NotAnArrowFnType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts", + "lineNumber": 78, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/fns.ts#L78" + }, + "signature": [ + "(a: string, b: number | undefined, c: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + ", d: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ", e: string | undefined) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aUnionProperty", + "type": "CompoundType", + "label": "aUnionProperty", + "description": [ + "\nThis is a complicated union type" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 51, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L51" + }, + "signature": [ + "string | number | (() => string) | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.CrazyClass", + "text": "CrazyClass" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aStrArray", + "type": "Array", + "label": "aStrArray", + "description": [ + "\nThis is an array of strings. The type is explicit." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 56, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L56" + }, + "signature": [ + "string[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aNumArray", + "type": "Array", + "label": "aNumArray", + "description": [ + "\nThis is an array of numbers. The type is implied." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 61, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L61" + }, + "signature": [ + "number[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.aStr", + "type": "string", + "label": "aStr", + "description": [ + "\nA string that says hi to you!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 66, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L66" + }, + "initialIsOpen": false + }, + { + "id": "def-public.aNum", + "type": "number", + "label": "aNum", + "description": [ + "\nIt's a number. A special number." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 71, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L71" + }, + "signature": [ + "10" + ], + "initialIsOpen": false + }, + { + "id": "def-public.literalString", + "type": "string", + "label": "literalString", + "description": [ + "\nI'm a type of string, but more specifically, a literal string type." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 76, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L76" + }, + "signature": [ + "\"HI\"" + ], + "initialIsOpen": false + }, + { + "id": "def-public.StringOrUndefinedType", + "type": "Type", + "label": "StringOrUndefinedType", + "description": [ + "\nHow should a potentially undefined type show up." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 15, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L15" + }, + "signature": [ + "undefined | string" + ], + "initialIsOpen": false + }, + { + "id": "def-public.TypeWithGeneric", + "type": "Type", + "label": "TypeWithGeneric", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 17, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L17" + }, + "signature": [ + "T[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.ImAType", + "type": "Type", + "label": "ImAType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 19, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L19" + }, + "signature": [ + "string | number | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAFooPluginApi", + "section": "def-public.FooType", + "text": "FooType" + }, + " | ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + " | ", + { + "pluginId": "pluginA", + "scope": "common", + "docId": "kibPluginAPluginApi", + "section": "def-common.ImACommonType", + "text": "ImACommonType" + } + ], + "initialIsOpen": false + }, + { + "id": "def-public.FnWithGeneric", + "type": "Type", + "label": "FnWithGeneric", + "description": [ + "\nThis is a type that defines a function.\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L26" + }, + "signature": [ + "(t: T) => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.TypeWithGeneric", + "text": "TypeWithGeneric" + }, + "" + ], + "initialIsOpen": false + }, + { + "id": "def-public.MultipleDeclarationsType", + "type": "Type", + "label": "MultipleDeclarationsType", + "description": [ + "\nCalling node.getSymbol().getDeclarations() will return > 1 declaration." + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 40, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L40" + }, + "signature": [ + "(typeof DayOfWeek)[]" + ], + "initialIsOpen": false + }, + { + "id": "def-public.IRefANotExportedType", + "type": "Type", + "label": "IRefANotExportedType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts", + "lineNumber": 42, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/types.ts#L42" + }, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAFooPluginApi", + "section": "def-public.ImNotExportedFromIndex", + "text": "ImNotExportedFromIndex" + }, + " | { zed: \"hi\"; }" + ], + "initialIsOpen": false + } + ], + "objects": [ + { + "id": "def-public.aPretendNamespaceObj", + "type": "Object", + "children": [ + { + "id": "def-public.aPretendNamespaceObj.notAnArrowFn", + "type": "Function", + "label": "notAnArrowFn", + "description": [ + "/**\n * The docs should show this inline comment.\n */" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 21, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L21" + }, + "signature": [ + "typeof ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.notAnArrowFn", + "text": "notAnArrowFn" + } + ] + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyMisdirection", + "type": "Function", + "label": "aPropertyMisdirection", + "description": [ + "/**\n * Should this comment show up?\n */" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 26, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L26" + }, + "signature": [ + "typeof ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.notAnArrowFn", + "text": "notAnArrowFn" + } + ] + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyInlineFn", + "type": "Function", + "children": [ + { + "type": "CompoundType", + "label": "a", + "isRequired": true, + "signature": [ + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 31, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31" + } + } + ], + "signature": [ + "(a: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + }, + ") => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.ImAType", + "text": "ImAType" + } + ], + "description": [ + "/**\n * I'm a property inline fun.\n */" + ], + "label": "aPropertyInlineFn", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 31, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L31" + }, + "returnComment": [] + }, + { + "id": "def-public.aPretendNamespaceObj.aPropertyStr", + "type": "string", + "label": "aPropertyStr", + "description": [ + "/**\n * The only way for this to have a comment is to grab this.\n */" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 38, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L38" + } + }, + { + "id": "def-public.aPretendNamespaceObj.nestedObj", + "type": "Object", + "children": [ + { + "id": "def-public.aPretendNamespaceObj.nestedObj.foo", + "type": "string", + "label": "foo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 44, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L44" + } + } + ], + "description": [ + "/**\n * Will this nested object have it's children extracted appropriately?\n */" + ], + "label": "nestedObj", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 43, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L43" + } + } + ], + "description": [ + "\nSome of the plugins wrap static exports in an object to create\na namespace like this." + ], + "label": "aPretendNamespaceObj", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts", + "lineNumber": 17, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/const_vars.ts#L17" + }, + "initialIsOpen": false + } + ], + "setup": { + "id": "def-public.Setup", + "type": "Interface", + "label": "Setup", + "description": [ + "\nAccess setup functionality from your plugin's setup function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n setup(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchService();\n }\n}\n```" + ], + "children": [ + { + "id": "def-public.Setup.getSearchService", + "type": "Function", + "label": "getSearchService", + "description": [ + "\nA factory function that returns a new instance of Foo based\non the spec. We aren't sure if this is a good function so it's marked\nbeta. That should be clear in the docs because of the js doc tag.\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 96, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L96" + }, + "signature": [ + "(searchSpec: ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.SearchSpec", + "text": "SearchSpec" + }, + ") => string" + ] + }, + { + "id": "def-public.Setup.getSearchService2", + "type": "Function", + "label": "getSearchService2", + "description": [ + "\nThis uses an inlined object type rather than referencing an exported type, which is discouraged.\nprefer the way {@link getSearchService} is typed.\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 104, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L104" + }, + "signature": [ + "(searchSpec: { username: string; password: string; }) => string" + ] + }, + { + "id": "def-public.Setup.doTheThing", + "type": "Function", + "label": "doTheThing", + "description": [ + "\nThis function does the thing and it's so good at it! But we decided to deprecate it\nanyway. I hope that's clear to developers in the docs!\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 117, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L117" + }, + "signature": [ + "(thingOne: number, thingTwo: string, thingThree: { nestedVar: number; }) => void" + ] + }, + { + "id": "def-public.Setup.fnWithInlineParams", + "type": "Function", + "label": "fnWithInlineParams", + "description": [ + "\nWho would write such a complicated function?? Ew, how will the obj parameter appear in docs?\n" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 128, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L128" + }, + "signature": [ + "(obj: { fn: (foo: { param: string; }) => number; }) => () => { retFoo: () => string; }" + ] + }, + { + "id": "def-public.Setup.id", + "type": "string", + "label": "id", + "description": [ + "\nHi, I'm a comment for an id string!" + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 135, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L135" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 84, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L84" + }, + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "id": "def-public.Start", + "type": "Interface", + "label": "Start", + "description": [ + "\nAccess start functionality from your plugin's start function by adding the example\nplugin as a dependency.\n\n```ts\nClass MyPlugin {\n start(core: CoreDependencies, { example }: PluginDependencies) {\n // Here you can access this functionality.\n example.getSearchLanguage();\n }\n}\n```" + ], + "children": [ + { + "id": "def-public.Start.getSearchLanguage", + "type": "Function", + "label": "getSearchLanguage", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 68, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L68" + }, + "signature": [ + "() => ", + { + "pluginId": "pluginA", + "scope": "public", + "docId": "kibPluginAPluginApi", + "section": "def-public.SearchLanguage", + "text": "SearchLanguage" + } + ] + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts", + "lineNumber": 64, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/plugin.ts#L64" + }, + "lifecycle": "start", + "initialIsOpen": true + } + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "id": "def-common.ImACommonType", + "type": "Interface", + "label": "ImACommonType", + "description": [], + "children": [ + { + "id": "def-common.ImACommonType.goo", + "type": "number", + "label": "goo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts", + "lineNumber": 12, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L12" + } + } + ], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts", + "lineNumber": 11, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/index.ts#L11" + }, + "initialIsOpen": false + } + ], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json index 8b5ec5f3da960..2589948b54ff0 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.json @@ -1 +1,77 @@ -{"id":"pluginA.foo","client":{"classes":[],"functions":[{"id":"def-public.doTheFooFnThing","type":"Function","children":[],"signature":["() => void"],"description":[],"label":"doTheFooFnThing","source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9"},"returnComment":[],"initialIsOpen":false}],"interfaces":[],"enums":[],"misc":[{"id":"def-public.FooType","type":"Type","label":"FooType","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts","lineNumber":11,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11"},"signature":["() => \"foo\""],"initialIsOpen":false}],"objects":[]},"server":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[],"objects":[]},"common":{"classes":[],"functions":[],"interfaces":[],"enums":[],"misc":[{"id":"def-common.commonFoo","type":"string","label":"commonFoo","description":[],"source":{"path":"/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts","lineNumber":9,"link":"https://github.com/elastic/kibana/tree/master/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9"},"signature":["\"COMMON VAR!\""],"initialIsOpen":false}],"objects":[]}} \ No newline at end of file +{ + "id": "pluginA.foo", + "client": { + "classes": [], + "functions": [ + { + "id": "def-public.doTheFooFnThing", + "type": "Function", + "children": [], + "signature": [ + "() => void" + ], + "description": [], + "label": "doTheFooFnThing", + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "lineNumber": 9, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L9" + }, + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [ + { + "id": "def-public.FooType", + "type": "Type", + "label": "FooType", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", + "lineNumber": 11, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts#L11" + }, + "signature": [ + "() => \"foo\"" + ], + "initialIsOpen": false + } + ], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "id": "def-common.commonFoo", + "type": "string", + "label": "commonFoo", + "description": [], + "source": { + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts", + "lineNumber": 9, + "link": "https://github.com/elastic/kibana/tree/masterpackages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts#L9" + }, + "signature": [ + "\"COMMON VAR!\"" + ], + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/types.ts b/packages/kbn-docs-utils/src/api_docs/types.ts index c41cd42e6b424..5468709206eec 100644 --- a/packages/kbn-docs-utils/src/api_docs/types.ts +++ b/packages/kbn-docs-utils/src/api_docs/types.ts @@ -97,7 +97,7 @@ export interface Reference { pluginId: string; scope: ApiScope; docId: string; - section: string; + section?: string; text: string; } diff --git a/packages/kbn-docs-utils/src/api_docs/utils.ts b/packages/kbn-docs-utils/src/api_docs/utils.ts index 34162aa330911..66cdfee8f233b 100644 --- a/packages/kbn-docs-utils/src/api_docs/utils.ts +++ b/packages/kbn-docs-utils/src/api_docs/utils.ts @@ -91,9 +91,6 @@ export function getPluginApiDocId( const cleanName = id.replace('.', '_'); if (serviceInfo) { const serviceName = getServiceForPath(serviceInfo.apiPath, serviceInfo.directory); - log.debug( - `Service for path ${serviceInfo.apiPath} and ${serviceInfo.directory} is ${serviceName}` - ); const serviceFolder = serviceInfo.serviceFolders?.find((f) => f === serviceName); if (serviceFolder) { diff --git a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts index 300c9f4dd66b0..2c36e24453c62 100644 --- a/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts +++ b/packages/kbn-es-archiver/src/actions/empty_kibana_index.ts @@ -7,7 +7,8 @@ */ import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { migrateKibanaIndex, createStats, cleanKibanaIndices } from '../lib'; diff --git a/packages/kbn-es-archiver/src/actions/load.ts b/packages/kbn-es-archiver/src/actions/load.ts index 60af6b3aa747b..68d5437336023 100644 --- a/packages/kbn-es-archiver/src/actions/load.ts +++ b/packages/kbn-es-archiver/src/actions/load.ts @@ -9,7 +9,8 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; import { Readable } from 'stream'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { Client } from '@elastic/elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '@kbn/utils'; import { ES_CLIENT_HEADERS } from '../client_headers'; diff --git a/packages/kbn-es-archiver/src/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts index c12fa935f786a..b5f259a1496bb 100644 --- a/packages/kbn-es-archiver/src/actions/unload.ts +++ b/packages/kbn-es-archiver/src/actions/unload.ts @@ -10,7 +10,8 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; import { Readable, Writable } from 'stream'; import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { createPromiseFromStreams } from '@kbn/utils'; import { diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index 919aff5c3851b..9617457d4573e 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -17,8 +17,8 @@ import Url from 'url'; import readline from 'readline'; import Fs from 'fs'; -import { RunWithCommands, createFlagError, KbnClient, CA_CERT_PATH } from '@kbn/dev-utils'; -import { readConfigFile } from '@kbn/test'; +import { RunWithCommands, createFlagError, CA_CERT_PATH } from '@kbn/dev-utils'; +import { readConfigFile, KbnClient } from '@kbn/test'; import { Client } from '@elastic/elasticsearch'; import { EsArchiver } from './es_archiver'; diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index b00b9fb8b3f25..68eacb4f3caf2 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -7,7 +7,8 @@ */ import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { saveAction, diff --git a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts index 7f0080783ee0a..dc49085cbd458 100644 --- a/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts +++ b/packages/kbn-es-archiver/src/lib/indices/kibana_index.ts @@ -9,7 +9,8 @@ import { inspect } from 'util'; import { Client } from '@elastic/elasticsearch'; -import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { ES_CLIENT_HEADERS } from '../../client_headers'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 4fd28678d2653..0694bc4ffdb0f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -213,6 +213,13 @@ export const schema = Joi.object() }) .default(), + // settings for the saved objects svc + kbnArchiver: Joi.object() + .keys({ + directory: Joi.string().default(defaultRelativeToConfigPath('fixtures/kbn_archiver')), + }) + .default(), + // settings for the kibanaServer.uiSettings module uiSettings: Joi.object() .keys({ diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 25a5c6541bf07..919dc8b4477f3 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -48,3 +48,7 @@ export { getUrl } from './jest/utils/get_url'; export { runCheckJestConfigsCli } from './jest/run_check_jest_configs_cli'; export { runJest } from './jest/run'; + +export * from './kbn_archiver_cli'; + +export * from './kbn_client'; diff --git a/packages/kbn-test/src/kbn_archiver_cli.ts b/packages/kbn-test/src/kbn_archiver_cli.ts new file mode 100644 index 0000000000000..98bfa6eaa4046 --- /dev/null +++ b/packages/kbn-test/src/kbn_archiver_cli.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Url from 'url'; + +import { RunWithCommands, createFlagError, Flags } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; + +import { readConfigFile } from './functional_test_runner'; + +function getSinglePositionalArg(flags: Flags) { + const positional = flags._; + if (positional.length < 1) { + throw createFlagError('missing name of export to import'); + } + + if (positional.length > 1) { + throw createFlagError(`extra positional arguments, expected 1, got [${positional}]`); + } + + return positional[0]; +} + +function parseTypesFlag(flags: Flags) { + if (!flags.type || (typeof flags.type !== 'string' && !Array.isArray(flags.type))) { + throw createFlagError('--type is a required flag'); + } + + const types = typeof flags.type === 'string' ? [flags.type] : flags.type; + return types.reduce( + (acc: string[], type) => [...acc, ...type.split(',').map((t) => t.trim())], + [] + ); +} + +export function runKbnArchiverCli() { + new RunWithCommands({ + description: 'Import/export saved objects from archives, for testing', + globalFlags: { + string: ['config', 'space', 'kibana-url', 'dir'], + help: ` + --space space id to operate on, defaults to the default space + --config optional path to an FTR config file that will be parsed and used for defaults + --kibana-url set the url that kibana can be reached at, uses the "servers.kibana" setting from --config by default + --dir directory that contains exports to be imported, or where exports will be saved, uses the "kbnArchiver.directory" + setting from --config by default + `, + }, + async extendContext({ log, flags }) { + let config; + if (flags.config) { + if (typeof flags.config !== 'string') { + throw createFlagError('expected --config to be a string'); + } + + config = await readConfigFile(log, Path.resolve(flags.config)); + } + + let kibanaUrl; + if (flags['kibana-url']) { + if (typeof flags['kibana-url'] !== 'string') { + throw createFlagError('expected --kibana-url to be a string'); + } + + kibanaUrl = flags['kibana-url']; + } else if (config) { + kibanaUrl = Url.format(config.get('servers.kibana')); + } + + if (!kibanaUrl) { + throw createFlagError( + 'Either a --config file with `servers.kibana` defined, or a --kibana-url must be passed' + ); + } + + let importExportDir; + if (flags.dir) { + if (typeof flags.dir !== 'string') { + throw createFlagError('expected --dir to be a string'); + } + + importExportDir = flags.dir; + } else if (config) { + importExportDir = config.get('kbnArchiver.directory'); + } + + if (!importExportDir) { + throw createFlagError( + '--config does not include a kbnArchiver.directory, specify it or include --dir flag' + ); + } + + const space = flags.space; + if (!(space === undefined || typeof space === 'string')) { + throw createFlagError('--space must be a string'); + } + + return { + space, + kbnClient: new KbnClient({ + log, + url: kibanaUrl, + importExportDir, + }), + }; + }, + }) + .command({ + name: 'save', + usage: 'save ', + description: 'export saved objects from Kibana to a file', + flags: { + string: ['type'], + help: ` + --type saved object type that should be fetched and stored in the archive, can + be specified multiple times or be a comma-separated list. + `, + }, + async run({ kbnClient, flags, space }) { + await kbnClient.importExport.save(getSinglePositionalArg(flags), { + types: parseTypesFlag(flags), + space, + }); + }, + }) + .command({ + name: 'load', + usage: 'load ', + description: 'import a saved export to Kibana', + async run({ kbnClient, flags, space }) { + await kbnClient.importExport.load(getSinglePositionalArg(flags), { space }); + }, + }) + .command({ + name: 'unload', + usage: 'unload ', + description: 'delete the saved objects saved in the archive from the Kibana index', + async run({ kbnClient, flags, space }) { + await kbnClient.importExport.unload(getSinglePositionalArg(flags), { space }); + }, + }) + .execute(); +} diff --git a/packages/kbn-dev-utils/src/kbn_client/index.ts b/packages/kbn-test/src/kbn_client/index.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/index.ts rename to packages/kbn-test/src/kbn_client/index.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-test/src/kbn_client/kbn_client.ts similarity index 87% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client.ts rename to packages/kbn-test/src/kbn_client/kbn_client.ts index 963639d47045b..3fa74412c1a8b 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client.ts @@ -6,19 +6,22 @@ * Side Public License, v 1. */ -import { ToolingLog } from '../tooling_log'; +import { ToolingLog } from '@kbn/dev-utils'; + import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; import { KbnClientStatus } from './kbn_client_status'; import { KbnClientPlugins } from './kbn_client_plugins'; import { KbnClientVersion } from './kbn_client_version'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; +import { KbnClientImportExport } from './kbn_client_import_export'; export interface KbnClientOptions { url: string; certificateAuthorities?: Buffer[]; log: ToolingLog; uiSettingDefaults?: UiSettingValues; + importExportDir?: string; } export class KbnClient { @@ -27,6 +30,7 @@ export class KbnClient { readonly version: KbnClientVersion; readonly savedObjects: KbnClientSavedObjects; readonly uiSettings: KbnClientUiSettings; + readonly importExport: KbnClientImportExport; private readonly requester: KbnClientRequester; private readonly log: ToolingLog; @@ -56,6 +60,12 @@ export class KbnClient { this.version = new KbnClientVersion(this.status); this.savedObjects = new KbnClientSavedObjects(this.log, this.requester); this.uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + this.importExport = new KbnClientImportExport( + this.log, + this.requester, + this.savedObjects, + options.importExportDir + ); } /** diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts new file mode 100644 index 0000000000000..bb5b99fdc4439 --- /dev/null +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { inspect } from 'util'; +import Fs from 'fs/promises'; +import Path from 'path'; + +import FormData from 'form-data'; +import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; + +import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; +import { KbnClientSavedObjects } from './kbn_client_saved_objects'; + +interface ImportApiResponse { + success: boolean; + [key: string]: unknown; +} + +interface SavedObject { + id: string; + type: string; + [key: string]: unknown; +} + +async function parseArchive(path: string): Promise { + return (await Fs.readFile(path, 'utf-8')) + .split('\n\n') + .filter((line) => !!line) + .map((line) => JSON.parse(line)); +} + +export class KbnClientImportExport { + constructor( + public readonly log: ToolingLog, + public readonly requester: KbnClientRequester, + public readonly savedObjects: KbnClientSavedObjects, + public readonly dir?: string + ) {} + + private resolvePath(path: string) { + if (!Path.extname(path)) { + path = `${path}.json`; + } + + if (!this.dir && !Path.isAbsolute(path)) { + throw new Error( + 'unable to resolve relative path to import/export without a configured dir, either path absolute path or specify --dir' + ); + } + + return this.dir ? Path.resolve(this.dir, path) : path; + } + + async load(name: string, options?: { space?: string }) { + const src = this.resolvePath(name); + this.log.debug('resolved import for', name, 'to', src); + + const objects = await parseArchive(src); + this.log.info('importing', objects.length, 'saved objects', { space: options?.space }); + + const formData = new FormData(); + formData.append('file', objects.map((obj) => JSON.stringify(obj)).join('\n'), 'import.ndjson'); + + // TODO: should we clear out the existing saved objects? + const resp = await this.req(options?.space, { + method: 'POST', + path: '/api/saved_objects/_import', + query: { + overwrite: true, + }, + body: formData, + headers: formData.getHeaders(), + }); + + if (resp.data.success) { + this.log.success('import success'); + } else { + throw createFailError(`failed to import all saved objects: ${inspect(resp.data)}`); + } + } + + async unload(name: string, options?: { space?: string }) { + const src = this.resolvePath(name); + this.log.debug('unloading docs from archive at', src); + + const objects = await parseArchive(src); + this.log.info('deleting', objects.length, 'objects', { space: options?.space }); + + const { deleted, missing } = await this.savedObjects.bulkDelete({ + space: options?.space, + objects, + }); + + if (missing) { + this.log.info(missing, 'saved objects were already deleted'); + } + + this.log.success(deleted, 'saved objects deleted'); + } + + async save(name: string, options: { types: string[]; space?: string }) { + const dest = this.resolvePath(name); + this.log.debug('saving export to', dest); + + const resp = await this.req(options.space, { + method: 'POST', + path: '/api/saved_objects/_export', + body: { + type: options.types, + excludeExportDetails: true, + includeReferencesDeep: true, + }, + }); + + if (typeof resp.data !== 'string') { + throw createFailError(`unexpected response from export API: ${inspect(resp.data)}`); + } + + const objects = resp.data + .split('\n') + .filter((l) => !!l) + .map((line) => JSON.parse(line)); + + const fileContents = objects + .map((obj) => { + const { sort: _, ...nonSortFields } = obj; + return JSON.stringify(nonSortFields, null, 2); + }) + .join('\n\n'); + + await Fs.writeFile(dest, fileContents, 'utf-8'); + + this.log.success('Exported', objects.length, 'saved objects to', dest); + } + + private async req(space: string | undefined, options: ReqOptions) { + if (!options.path.startsWith('/')) { + throw new Error('options.path must start with a /'); + } + + try { + return await this.requester.request({ + ...options, + path: space ? uriencode`/s/${space}` + options.path : options.path, + }); + } catch (error) { + if (!isAxiosResponseError(error)) { + throw error; + } + + throw createFailError( + `${error.response.status} resp: ${inspect(error.response.data)}\nreq: ${inspect( + error.config + )}` + ); + } + } +} diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts b/packages/kbn-test/src/kbn_client/kbn_client_plugins.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts rename to packages/kbn-test/src/kbn_client/kbn_client_plugins.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts similarity index 93% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts rename to packages/kbn-test/src/kbn_client/kbn_client_requester.ts index d940525f57e3c..2e1575aee1897 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -8,10 +8,10 @@ import Url from 'url'; import Https from 'https'; -import Axios, { AxiosResponse } from 'axios'; +import Qs from 'querystring'; -import { isAxiosRequestError, isAxiosResponseError } from '../axios'; -import { ToolingLog } from '../tooling_log'; +import Axios, { AxiosResponse } from 'axios'; +import { ToolingLog, isAxiosRequestError, isAxiosResponseError } from '@kbn/dev-utils'; const isConcliftOnGetError = (error: any) => { return ( @@ -52,6 +52,7 @@ export interface ReqOptions { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; retries?: number; + headers?: Record; } const delay = (ms: number) => @@ -102,9 +103,11 @@ export class KbnClientRequester { data: options.body, params: options.query, headers: { + ...options.headers, 'kbn-xsrf': 'kbn-client', }, httpsAgent: this.httpsAgent, + paramsSerializer: (params) => Qs.stringify(params), }); return response; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts similarity index 60% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts rename to packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 9d616d6f50a88..904ccc385bd7d 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { ToolingLog } from '../tooling_log'; +import { inspect } from 'util'; + +import * as Rx from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; +import { lastValueFrom } from '@kbn/std'; +import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; import { KbnClientRequester, uriencode } from './kbn_client_requester'; @@ -51,6 +56,38 @@ interface MigrateResponse { result: Array<{ status: string }>; } +interface FindApiResponse { + saved_objects: Array<{ + type: string; + id: string; + [key: string]: unknown; + }>; + total: number; + per_page: number; + page: number; +} + +interface CleanOptions { + space?: string; + types: string[]; +} + +interface DeleteObjectsOptions { + space?: string; + objects: Array<{ + type: string; + id: string; + }>; +} + +async function concurrently(maxConcurrency: number, arr: T[], fn: (item: T) => Promise) { + if (arr.length) { + await lastValueFrom( + Rx.from(arr).pipe(mergeMap(async (item) => await fn(item), maxConcurrency)) + ); + } +} + export class KbnClientSavedObjects { constructor(private readonly log: ToolingLog, private readonly requester: KbnClientRequester) {} @@ -143,4 +180,67 @@ export class KbnClientSavedObjects { return data; } + + public async clean(options: CleanOptions) { + this.log.debug('Cleaning all saved objects', { space: options.space }); + + let deleted = 0; + + while (true) { + const resp = await this.requester.request({ + method: 'GET', + path: options.space + ? uriencode`/s/${options.space}/api/saved_objects/_find` + : '/api/saved_objects/_find', + query: { + per_page: 1000, + type: options.types, + fields: 'none', + }, + }); + + this.log.info('deleting batch of', resp.data.saved_objects.length, 'objects'); + const deletion = await this.bulkDelete({ + space: options.space, + objects: resp.data.saved_objects, + }); + deleted += deletion.deleted; + + if (resp.data.total <= resp.data.per_page) { + break; + } + } + + this.log.success('deleted', deleted, 'objects'); + } + + public async bulkDelete(options: DeleteObjectsOptions) { + let deleted = 0; + let missing = 0; + + await concurrently(20, options.objects, async (obj) => { + try { + await this.requester.request({ + method: 'DELETE', + path: options.space + ? uriencode`/s/${options.space}/api/saved_objects/${obj.type}/${obj.id}` + : uriencode`/api/saved_objects/${obj.type}/${obj.id}`, + }); + deleted++; + } catch (error) { + if (isAxiosResponseError(error)) { + if (error.response.status === 404) { + missing++; + return; + } + + throw createFailError(`${error.response.status} resp: ${inspect(error.response.data)}`); + } + + throw error; + } + }); + + return { deleted, missing }; + } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-test/src/kbn_client/kbn_client_status.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts rename to packages/kbn-test/src/kbn_client/kbn_client_status.ts diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts similarity index 98% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts rename to packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts index 75fd7a4c8391e..78155098ef038 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_ui_settings.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ToolingLog } from '../tooling_log'; +import { ToolingLog } from '@kbn/dev-utils'; import { KbnClientRequester, uriencode } from './kbn_client_requester'; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts b/packages/kbn-test/src/kbn_client/kbn_client_version.ts similarity index 100% rename from packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts rename to packages/kbn-test/src/kbn_client/kbn_client_version.ts diff --git a/scripts/kbn_archiver.js b/scripts/kbn_archiver.js new file mode 100644 index 0000000000000..b04b86a0d4eed --- /dev/null +++ b/scripts/kbn_archiver.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/test').runKbnArchiverCli(); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index fc26c837d5e52..267d671361184 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -96,8 +96,7 @@ describe('getSearchDsl', () => { mappings, opts.type, opts.sortField, - opts.sortOrder, - opts.pit + opts.sortOrder ); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index cae5e43897bcf..9820544f02bd1 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -78,7 +78,7 @@ export function getSearchDsl( hasReferenceOperator, kueryNode, }), - ...getSortingParams(mappings, type, sortField, sortOrder, pit), + ...getSortingParams(mappings, type, sortField, sortOrder), ...(pit ? getPitParams(pit) : {}), ...(searchAfter ? { search_after: searchAfter } : {}), }; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts index 73c7065705fc5..1376f0d50a9da 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts @@ -79,11 +79,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect(getSortingParams(MAPPINGS, 'saved', 'title', undefined, { id: 'abc' }).sort).toEqual( - expect.arrayContaining([{ _shard_doc: 'asc' }]) - ); - }); }); describe('sortField is simple root property with multiple types', () => { it('returns correct params', () => { @@ -98,11 +93,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', undefined, { id: 'abc' }).sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is simple non-root property with multiple types', () => { it('returns correct params', () => { @@ -124,11 +114,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, 'saved', 'title.raw', undefined, { id: 'abc' }).sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is multi-field with single type as array', () => { it('returns correct params', () => { @@ -143,11 +128,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, ['saved'], 'title.raw', undefined, { id: 'abc' }).sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is root multi-field with multiple types', () => { it('returns correct params', () => { @@ -162,12 +142,6 @@ describe('searchDsl/getSortParams', () => { ], }); }); - it('appends tiebreaker when PIT is provided', () => { - expect( - getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', undefined, { id: 'abc' }) - .sort - ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); - }); }); describe('sortField is not-root multi-field with multiple types', () => { it('returns correct params', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts index abef9bfa0a300..e3bfba6a80f59 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts @@ -8,12 +8,6 @@ import Boom from '@hapi/boom'; import { getProperty, IndexMapping } from '../../../mappings'; -import { SavedObjectsPitParams } from '../../../types'; - -// TODO: The plan is for ES to automatically add this tiebreaker when -// using PIT. We should remove this logic once that is resolved. -// https://github.com/elastic/elasticsearch/issues/56828 -const ES_PROVIDED_TIEBREAKER = { _shard_doc: 'asc' }; const TOP_LEVEL_FIELDS = ['_id', '_score']; @@ -21,8 +15,7 @@ export function getSortingParams( mappings: IndexMapping, type: string | string[], sortField?: string, - sortOrder?: string, - pit?: SavedObjectsPitParams + sortOrder?: string ) { if (!sortField) { return {}; @@ -38,7 +31,6 @@ export function getSortingParams( order: sortOrder, }, }, - ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } @@ -59,7 +51,6 @@ export function getSortingParams( unmapped_type: rootField.type, }, }, - ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } @@ -84,7 +75,6 @@ export function getSortingParams( unmapped_type: field.type, }, }, - ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index f9c1e67c0540d..b685b32038f8e 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -87,3 +87,19 @@ if [ "$GIT_CHANGES" ]; then echo -e "$GIT_CHANGES\n" exit 1 fi + +### +### rebuild plugin api docs to ensure it's not out of date +### +echo " -- building api docs" +node scripts/build_api_docs + +### +### verify no api changes +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'node scripts/build_api_docs' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi \ No newline at end of file diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json new file mode 100644 index 0000000000000..f2ef49f6e13e2 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.get_component_template.json @@ -0,0 +1,16 @@ +{ + "indices.get_template": { + "url_params": { + "flat_settings": "__flag__", + "master_timeout": "", + "local": "__flag__" + }, + "methods": [ + "GET" + ], + "patterns": [ + "_component_template", + "_component_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-component-templates.html" } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json new file mode 100644 index 0000000000000..fbd5a0905d4d8 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/cluster.put_component_template.json @@ -0,0 +1,14 @@ +{ + "indices.put_template": { + "url_params": { + "create": "__flag__", + "master_timeout": "" + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_component_template/{name}" + ], + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-component-template.html" } +} diff --git a/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json new file mode 100644 index 0000000000000..7fdd9ff23fd17 --- /dev/null +++ b/src/plugins/console/server/lib/spec_definitions/json/overrides/cluster.put_component_template.json @@ -0,0 +1,13 @@ +{ + "indices.put_template": { + "data_autocomplete_rules": { + "template": {}, + "aliases": {}, + "settings": {}, + "mappings": {}, + "version": 0, + "_meta": {}, + "allow_auto_create": false + } + } +} diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index 31de1f2f6ed66..2455589cf69fc 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -19,7 +19,10 @@ const getSharingDataFields = async ( timeFieldName: string, hideTimeColumn: boolean ) => { - if (selectedFields.length === 1 && selectedFields[0] === '_source') { + if ( + selectedFields.length === 0 || + (selectedFields.length === 1 && selectedFields[0] === '_source') + ) { const fieldCounts = await getFieldCounts(); return { searchFields: undefined, diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts index 2e4ab90ee58e5..f44d4650da56a 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -49,7 +49,7 @@ export async function persistSavedSearch( if (state.grid) { savedSearch.grid = state.grid; } - if (state.hideChart) { + if (typeof state.hideChart !== 'undefined') { savedSearch.hideChart = state.hideChart; } diff --git a/src/plugins/embeddable/README.asciidoc b/src/plugins/embeddable/README.asciidoc index 007b16587e9f8..165dc37c56cb3 100644 --- a/src/plugins/embeddable/README.asciidoc +++ b/src/plugins/embeddable/README.asciidoc @@ -26,7 +26,7 @@ link:https://github.com/elastic/kibana/blob/master/src/plugins/embeddable/docs/R === API docs -===== Browser API +==== Browser API https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablesetup.md[Browser Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md[Browser Start contract] diff --git a/src/plugins/expressions/README.asciidoc b/src/plugins/expressions/README.asciidoc index e07f6e2909ab8..554e3bfcb6976 100644 --- a/src/plugins/expressions/README.asciidoc +++ b/src/plugins/expressions/README.asciidoc @@ -46,7 +46,7 @@ image::https://user-images.githubusercontent.com/9773803/74162514-3250a880-4c21- https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserversetup.md[Server Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserverstart.md[Server Start contract] -===== Browser API +==== Browser API https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicesetup.md[Browser Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsstart.md[Browser Start contract] diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 14e4d6034c1c2..353273d1372e6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -172,7 +172,7 @@ export class VegaBaseView { // Override URL sanitizer to prevent external data loading (if disabled) const vegaLoader = loader(); const originalSanitize = vegaLoader.sanitize.bind(vegaLoader); - vegaLoader.sanitize = (uri, options) => { + vegaLoader.sanitize = async (uri, options) => { if (uri.bypassToken === bypassToken) { // If uri has a bypass token, the uri was encoded by bypassExternalUrlCheck() above. // because user can only supply pure JSON data structure. @@ -189,7 +189,11 @@ export class VegaBaseView { }) ); } - return originalSanitize(uri, options); + const result = await originalSanitize(uri, options); + // This will allow Vega users to load images from any domain. + result.crossOrigin = null; + + return result; }; config.loader = vegaLoader; diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index fd670e288ff5b..a9e6020cf3ee8 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -20,6 +20,7 @@ export const uiSettingsConfig: Record> = { name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { defaultMessage: 'Legacy charts library', }), + requiresPageReload: true, value: false, description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 51f9dc9d00772..f366a864db980 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -7,7 +7,7 @@ */ import Url from 'url'; -import { KbnClient } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -22,6 +22,7 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) { url, certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), uiSettingDefaults: defaults, + importExportDir: config.get('kbnArchiver.directory'), }); if (defaults) { diff --git a/test/common/services/security/role.ts b/test/common/services/security/role.ts index 2aae5b2282940..420bed027f317 100644 --- a/test/common/services/security/role.ts +++ b/test/common/services/security/role.ts @@ -7,7 +7,8 @@ */ import util from 'util'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; export class Role { constructor(private log: ToolingLog, private kibanaServer: KbnClient) {} diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index c20ff7e327b64..af9204866ad47 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -7,7 +7,8 @@ */ import util from 'util'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; export class RoleMappings { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts index 0d12a0dae2e46..3bd31bb5ed186 100644 --- a/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -7,7 +7,8 @@ */ import util from 'util'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; export class User { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 9323c9e2fe70b..aeb02e5c30eb8 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -20,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const elasticChart = getService('elasticChart'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { defaultIndex: 'logstash-*', }; @@ -27,7 +28,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover test', function describeIndexTests() { before(async function () { log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); + + await kibanaServer.savedObjects.clean({ types: ['search'] }); + await kibanaServer.importExport.load('discover'); // and load a set of makelogs data await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts index 9a6692dc793d6..2a6096f8d1a78 100644 --- a/test/functional/apps/discover/_discover_histogram.ts +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -97,15 +97,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(canvasExists).to.be(false); await PageObjects.discover.saveSearch(savedSearch); await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.loadSavedSearch('persisted hidden histogram'); + await PageObjects.header.waitUntilLoadingHasFinished(); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(false); await testSubjects.click('discoverChartToggle'); canvasExists = await elasticChart.canvasExists(); expect(canvasExists).to.be(true); - await PageObjects.discover.clickResetSavedSearchButton(); + await PageObjects.discover.saveSearch('persisted hidden histogram'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.discover.loadSavedSearch('persisted hidden histogram'); await PageObjects.header.waitUntilLoadingHasFinished(); canvasExists = await elasticChart.canvasExists(); - expect(canvasExists).to.be(false); + expect(canvasExists).to.be(true); }); }); } diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.js index c7d333bd681d1..7ed15a6cddbca 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.js @@ -16,9 +16,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); - // this functionality is no longer functional as of 7.0 but still needs cleanup - // https://github.com/elastic/kibana/issues/74118 - describe.skip('filter scripted fields', function describeIndexTests() { + describe('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); @@ -29,8 +27,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { - await esArchiver.unload('management'); - await kibanaServer.uiSettings.replace({}); + await esArchiver.load('empty_kibana'); }); const scriptedPainlessFieldName = 'ram_pain1'; diff --git a/test/functional/fixtures/kbn_archiver/discover.json b/test/functional/fixtures/kbn_archiver/discover.json new file mode 100644 index 0000000000000..e861f875a2d9e --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/discover.json @@ -0,0 +1,51 @@ +{ + "attributes": { + "fieldAttrs": "{\"referer\":{\"customLabel\":\"Referer custom\"}}", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "8.0.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzQsMl0=" +} + +{ + "attributes": { + "columns": [ + "_source" + ], + "description": "A Saved Search Description", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "A Saved Search", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "version": "WzUsMl0=" +} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index c63d43b4cb6ad..865806cffe5bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -39,7 +39,7 @@ // "resolveJsonModule" allows for importing, extracting types from and generating .json files. "resolveJsonModule": true, // Disallow inconsistently-cased references to the same file. - "forceConsistentCasingInFileNames": true, + "forceConsistentCasingInFileNames": false, // Forbid unused local variables as the rule was deprecated by ts-lint "noUnusedLocals": true, // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index aab848d4555d2..07bad42a3bfa3 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -13,10 +13,15 @@ Table of Contents - [Kibana alerting](#kibana-alerting) - [Terminology](#terminology) - [Usage](#usage) + - [Alerts API keys](#alerts-api-keys) - [Limitations](#limitations) + - [Plugin status](#plugin-status) - [Alert types](#alert-types) - [Methods](#methods) - [Executor](#executor) + - [Licensing](#licensing) + - [Documentation](#documentation) + - [Tests](#tests) - [Example](#example) - [Role Based Access-Control](#role-based-access-control) - [Alert Navigation](#alert-navigation) @@ -50,6 +55,17 @@ A Kibana alert detects a condition and executes one or more actions when that co 2. Configure feature level privileges using RBAC 3. Create an alert using the RESTful API [Documentation](https://www.elastic.co/guide/en/kibana/master/alerts-api-update.html) (see alerts -> create). +## Alerts API keys + +When we create an alert, we generate a new API key. + +When we update, enable, or disable an alert, we must invalidate the old API key and create a new one. + +To manage the invalidation process for API keys, we use the saved object `api_key_pending_invalidation`. This object stores all API keys that were marked for invalidation when alerts were updated. +For security plugin invalidation, we schedule a task to check if the`api_key_pending_invalidation` saved object contains new API keys that are marked for invalidation earlier than the configured delay. The default value for running the task is 5 mins. +To change the schedule for the invalidation task, use the kibana.yml configuration option `xpack.alerts.invalidateApiKeysTask.interval`. +To change the default delay for the API key invalidation, use the kibana.yml configuration option `xpack.alerts.invalidateApiKeysTask.removalDelay`. + ## Limitations When security is enabled, an SSL connection to Elasticsearch is required in order to use alerting. @@ -64,6 +80,27 @@ Note that the `manage_own_api_key` cluster privilege is not enough - it can be u is unauthorized for user [user-name-here] ``` +## Plugin status + +The plugin status of an alert is customized by including information about checking failures for the framework decryption: +``` +core.status.set( + combineLatest([ + core.status.derivedStatus$, + getHealthStatusStream(startPlugins.taskManager), + ]).pipe( + map(([derivedStatus, healthStatus]) => { + if (healthStatus.level > derivedStatus.level) { + return healthStatus as ServiceStatus; + } else { + return derivedStatus; + } + }) + ) + ); +``` +To check for framework decryption failures, we use the task `alerting_health_check`, which runs every 60 minutes by default. To change the default schedule, use the kibana.yml configuration option `xpack.alerts.healthCheck.interval`. + ## Alert types ### Methods @@ -124,6 +161,19 @@ For example, if the `context` has one variable `foo` which is an object that has } ``` +## Licensing + +Currently most of the alerts are free features. But some alert types are subscription features, such as the tracking containment alert. + +## Documentation + +You should create documentation for the new alert type. Make an entry in the alert type index [`docs/user/alerting/alert-types.asciidoc`](../../../docs/user/alerting/alert-types.asciidoc) that points to a new document for the alert type that should be in the proper application directory. + +## Tests + +The alert type should have jest tests and optionaly functional tests. +In the the tests we recomend to test the expected alert execution result with a different input params, the structure of the created alert and the params validation. The rest will be guaranteed as a framework functionality. + ### Example This example receives server and threshold as parameters. It will read the CPU usage of the server and schedule actions to be executed (asynchronously by the task manager) if the reading is greater than the threshold. diff --git a/x-pack/plugins/apm/common/utils/as_mutable_array.ts b/x-pack/plugins/apm/common/utils/as_mutable_array.ts new file mode 100644 index 0000000000000..ce1d7e607ec4c --- /dev/null +++ b/x-pack/plugins/apm/common/utils/as_mutable_array.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Sometimes we use `as const` to have a more specific type, +// because TypeScript by default will widen the value type of an +// array literal. Consider the following example: +// +// const filter = [ +// { term: { 'agent.name': 'nodejs' } }, +// { range: { '@timestamp': { gte: 'now-15m ' }} +// ]; + +// The result value type will be: + +// const filter: ({ +// term: { +// 'agent.name'?: string +// }; +// range?: undefined +// } | { +// term?: undefined; +// range: { +// '@timestamp': { +// gte: string +// } +// } +// })[]; + +// This can sometimes leads to issues. In those cases, we can +// use `as const`. However, the Readonly type is not compatible +// with Array. This function returns a mutable version of a type. + +export function asMutableArray>( + arr: T +): T extends Readonly<[...infer U]> ? U : unknown[] { + return arr as any; +} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 5c9d79f37cc57..b86f0d40de137 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -37,6 +37,7 @@ interface Tab { key: string; href: string; text: ReactNode; + hidden?: boolean; render: () => ReactNode; } @@ -126,6 +127,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const profilingTab = { key: 'profiling', href: useServiceProfilingHref({ serviceName }), + hidden: !config.profilingEnabled, text: ( @@ -167,22 +169,20 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(metricsTab); } - tabs.push(serviceMapTab); - - if (config.profilingEnabled) { - tabs.push(profilingTab); - } + tabs.push(serviceMapTab, profilingTab); const selectedTab = tabs.find((serviceTab) => serviceTab.key === tab); return ( <> - {tabs.map(({ href, key, text }) => ( - - {text} - - ))} + {tabs + .filter((t) => !t.hidden) + .map(({ href, key, text }) => ( + + {text} + + ))}

diff --git a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts index dc07b89ec7807..502824135db2a 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts @@ -8,28 +8,10 @@ import { useState, useEffect } from 'react'; export function useLocalStorage(key: string, defaultValue: T) { - const [item, setItem] = useState(getFromStorage()); - - function getFromStorage() { - const storedItem = window.localStorage.getItem(key); - - let toStore: T = defaultValue; - - if (storedItem !== null) { - try { - toStore = JSON.parse(storedItem) as T; - } catch (err) { - window.localStorage.removeItem(key); - // eslint-disable-next-line no-console - console.log(`Unable to decode: ${key}`); - } - } - - return toStore; - } + const [item, setItem] = useState(getFromStorage(key, defaultValue)); const updateFromStorage = () => { - const storedItem = getFromStorage(); + const storedItem = getFromStorage(key, defaultValue); setItem(storedItem); }; @@ -51,5 +33,25 @@ export function useLocalStorage(key: string, defaultValue: T) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // item state must be updated with a new key or default value + useEffect(() => { + setItem(getFromStorage(key, defaultValue)); + }, [key, defaultValue]); + return [item, saveToStorage] as const; } + +function getFromStorage(keyName: string, defaultValue: T) { + const storedItem = window.localStorage.getItem(keyName); + + if (storedItem !== null) { + try { + return JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(keyName); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${keyName}`); + } + } + return defaultValue; +} diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index dec5be8da32f4..7e7e073c0d2f6 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -118,7 +118,6 @@ Array [ "environments": Object { "terms": Object { "field": "service.environment", - "missing": "", }, }, "outcomes": Object { @@ -197,6 +196,56 @@ Array [ "size": 0, }, }, + Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, + "body": Object { + "aggs": Object { + "services": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + }, + }, + "latest": Object { + "top_metrics": Object { + "metrics": Object { + "field": "agent.name", + }, + "sort": Object { + "@timestamp": "desc", + }, + }, + }, + }, + "terms": Object { + "field": "service.name", + "size": 500, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + "size": 0, + }, + }, ] `; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 5f0302035462c..10c7420d0f3b0 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -40,15 +40,15 @@ interface AggregationParams { kuery?: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; + maxNumServices: number; } -const MAX_NUMBER_OF_SERVICES = 500; - export async function getServiceTransactionStats({ environment, kuery, setup, searchAggregatedTransactions, + maxNumServices, }: AggregationParams) { return withApmSpan('get_service_transaction_stats', async () => { const { apmEventClient, start, end } = setup; @@ -92,7 +92,7 @@ export async function getServiceTransactionStats({ services: { terms: { field: SERVICE_NAME, - size: MAX_NUMBER_OF_SERVICES, + size: maxNumServices, }, aggs: { transactionType: { @@ -104,7 +104,6 @@ export async function getServiceTransactionStats({ environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: '', }, }, sample: { @@ -147,9 +146,9 @@ export async function getServiceTransactionStats({ return { serviceName: bucket.key as string, transactionType: topTransactionTypeBucket.key as string, - environments: topTransactionTypeBucket.environments.buckets - .map((environmentBucket) => environmentBucket.key as string) - .filter(Boolean), + environments: topTransactionTypeBucket.environments.buckets.map( + (environmentBucket) => environmentBucket.key as string + ), agentName: topTransactionTypeBucket.sample.top[0].metrics[ AGENT_NAME ] as AgentName, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.ts new file mode 100644 index 0000000000000..cabd44c1e6907 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_from_metric_documents.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { + AGENT_NAME, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { environmentQuery, kqlQuery, rangeQuery } from '../../../utils/queries'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export function getServicesFromMetricDocuments({ + environment, + setup, + maxNumServices, + kuery, +}: { + setup: Setup & SetupTimeRange; + environment?: string; + maxNumServices: number; + kuery?: string; +}) { + return withApmSpan('get_services_from_metric_documents', async () => { + const { apmEventClient, start, end } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: maxNumServices, + }, + aggs: { + environments: { + terms: { + field: SERVICE_ENVIRONMENT, + }, + }, + latest: { + top_metrics: { + metrics: { field: AGENT_NAME } as const, + sort: { '@timestamp': 'desc' }, + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.services.buckets.map((bucket) => { + return { + serviceName: bucket.key as string, + environments: bucket.environments.buckets.map( + (envBucket) => envBucket.key as string + ), + agentName: bucket.latest.top[0].metrics[AGENT_NAME] as AgentName, + }; + }) ?? [] + ); + }); +} diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 1ddc7a6583c81..3b9792519d261 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -6,15 +6,18 @@ */ import { Logger } from '@kbn/logging'; +import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { joinByKey } from '../../../../common/utils/join_by_key'; -import { getServicesProjection } from '../../../projections/services'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getHealthStatuses } from './get_health_statuses'; +import { getServicesFromMetricDocuments } from './get_services_from_metric_documents'; import { getServiceTransactionStats } from './get_service_transaction_stats'; export type ServicesItemsSetup = Setup & SetupTimeRange; +const MAX_NUMBER_OF_SERVICES = 500; + export async function getServicesItems({ environment, kuery, @@ -32,33 +35,49 @@ export async function getServicesItems({ const params = { environment, kuery, - projection: getServicesProjection({ - kuery, - setup, - searchAggregatedTransactions, - }), setup, searchAggregatedTransactions, + maxNumServices: MAX_NUMBER_OF_SERVICES, }; - const [transactionStats, healthStatuses] = await Promise.all([ + const [ + transactionStats, + servicesFromMetricDocuments, + healthStatuses, + ] = await Promise.all([ getServiceTransactionStats(params), + getServicesFromMetricDocuments(params), getHealthStatuses(params).catch((err) => { logger.error(err); return []; }), ]); - const apmServices = transactionStats.map(({ serviceName }) => serviceName); + const foundServiceNames = transactionStats.map( + ({ serviceName }) => serviceName + ); + + const servicesWithOnlyMetricDocuments = servicesFromMetricDocuments.filter( + ({ serviceName }) => !foundServiceNames.includes(serviceName) + ); + + const allServiceNames = foundServiceNames.concat( + servicesWithOnlyMetricDocuments.map(({ serviceName }) => serviceName) + ); // make sure to exclude health statuses from services // that are not found in APM data const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => - apmServices.includes(serviceName) + allServiceNames.includes(serviceName) ); - const allMetrics = [...transactionStats, ...matchedHealthStatuses]; - - return joinByKey(allMetrics, 'serviceName'); + return joinByKey( + asMutableArray([ + ...transactionStats, + ...servicesWithOnlyMetricDocuments, + ...matchedHealthStatuses, + ] as const), + 'serviceName' + ); }); } diff --git a/x-pack/plugins/case/common/api/cases/commentable_case.ts b/x-pack/plugins/case/common/api/cases/commentable_case.ts deleted file mode 100644 index 023229a90d352..0000000000000 --- a/x-pack/plugins/case/common/api/cases/commentable_case.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { CaseAttributesRt } from './case'; -import { CommentResponseRt } from './comment'; -import { SubCaseAttributesRt, SubCaseResponseRt } from './sub_case'; - -export const CollectionSubCaseAttributesRt = rt.intersection([ - rt.partial({ subCase: SubCaseAttributesRt }), - rt.type({ - case: CaseAttributesRt, - }), -]); - -export const CollectWithSubCaseResponseRt = rt.intersection([ - CaseAttributesRt, - rt.type({ - id: rt.string, - totalComment: rt.number, - version: rt.string, - }), - rt.partial({ - subCase: SubCaseResponseRt, - totalAlerts: rt.number, - comments: rt.array(CommentResponseRt), - }), -]); - -export type CollectionWithSubCaseResponse = rt.TypeOf; -export type CollectionWithSubCaseAttributes = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/cases/index.ts b/x-pack/plugins/case/common/api/cases/index.ts index 4d1fc68109ddb..6e7fb818cb2b5 100644 --- a/x-pack/plugins/case/common/api/cases/index.ts +++ b/x-pack/plugins/case/common/api/cases/index.ts @@ -11,4 +11,3 @@ export * from './comment'; export * from './status'; export * from './user_actions'; export * from './sub_case'; -export * from './commentable_case'; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index 00c8ff402c802..43e292b91db4b 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -24,8 +24,8 @@ export const getSubCasesUrl = (caseID: string): string => { return SUB_CASES_URL.replace('{case_id}', caseID); }; -export const getSubCaseDetailsUrl = (caseID: string, subCaseID: string): string => { - return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID); +export const getSubCaseDetailsUrl = (caseID: string, subCaseId: string): string => { + return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId); }; export const getCaseCommentsUrl = (id: string): string => { @@ -40,8 +40,8 @@ export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; -export const getSubCaseUserActionUrl = (caseID: string, subCaseID: string): string => { - return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID); +export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): string => { + return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId); }; export const getCasePushUrl = (caseId: string, connectorId: string): string => { diff --git a/x-pack/plugins/case/common/api/runtime_types.ts b/x-pack/plugins/case/common/api/runtime_types.ts index 43e3be04d10e5..b2ff763838287 100644 --- a/x-pack/plugins/case/common/api/runtime_types.ts +++ b/x-pack/plugins/case/common/api/runtime_types.ts @@ -9,14 +9,37 @@ import { either, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; +import { isObject } from 'lodash/fp'; type ErrorFactory = (message: string) => Error; +export const formatErrors = (errors: rt.Errors): string[] => { + const err = errors.map((error) => { + if (error.message != null) { + return error.message; + } else { + const keyContext = error.context + .filter( + (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' + ) + .map((entry) => entry.key) + .join(','); + + const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); + const suppliedValue = + keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; + const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; + return `Invalid value "${value}" supplied to "${suppliedValue}"`; + } + }); + + return [...new Set(err)]; +}; + export const createPlainError = (message: string) => new Error(message); export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { - throw createError(failure(errors).join('\n')); + throw createError(formatErrors(errors).join()); }; export const decodeOrThrow = ( diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts index a7ca5d9742c6b..0b2663b737204 100644 --- a/x-pack/plugins/case/server/client/alerts/get.ts +++ b/x-pack/plugins/case/server/client/alerts/get.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { AlertServiceContract } from '../../services'; import { CaseClientGetAlertsResponse } from './types'; @@ -14,6 +14,7 @@ interface GetParams { ids: string[]; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } export const get = async ({ @@ -21,12 +22,13 @@ export const get = async ({ ids, indices, scopedClusterClient, + logger, }: GetParams): Promise => { if (ids.length === 0 || indices.size <= 0) { return []; } - const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient }); + const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient, logger }); if (!alerts) { return []; } diff --git a/x-pack/plugins/case/server/client/alerts/update_status.test.ts b/x-pack/plugins/case/server/client/alerts/update_status.test.ts index c8df1c8ab74f3..b3ed3c2b84a99 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.test.ts @@ -22,6 +22,7 @@ describe('updateAlertsStatus', () => { expect(caseClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({ scopedClusterClient: expect.anything(), + logger: expect.anything(), ids: ['alert-id-1'], indices: new Set(['.siem-signals']), status: CaseStatuses.closed, diff --git a/x-pack/plugins/case/server/client/alerts/update_status.ts b/x-pack/plugins/case/server/client/alerts/update_status.ts index cb18bd4fc16e3..2194c3a18afdd 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient } from 'src/core/server'; +import { ElasticsearchClient, Logger } from 'src/core/server'; import { CaseStatuses } from '../../../common/api'; import { AlertServiceContract } from '../../services'; @@ -15,6 +15,7 @@ interface UpdateAlertsStatusArgs { status: CaseStatuses; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } export const updateAlertsStatus = async ({ @@ -23,6 +24,7 @@ export const updateAlertsStatus = async ({ status, indices, scopedClusterClient, + logger, }: UpdateAlertsStatusArgs): Promise => { - await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient }); + await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 3016a57f21875..e8cc1a8898f04 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -6,6 +6,7 @@ */ import { ConnectorTypes, CaseStatuses, CaseType, CaseClientPostRequest } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, @@ -276,14 +277,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing description', async () => { @@ -303,14 +306,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing tags', async () => { @@ -330,14 +335,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing connector ', async () => { @@ -352,14 +359,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when connector missing the right fields', async () => { @@ -380,14 +389,16 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .create({ theCase: postCase }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .create({ theCase: postCase }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws if you passing status for a new case', async () => { @@ -413,7 +424,7 @@ describe('create', () => { caseSavedObject: mockCases, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.create(postCase).catch((e) => { + return caseClient.client.create(postCase).catch((e) => { expect(e).not.toBeNull(); expect(e.isBoom).toBe(true); expect(e.output.statusCode).toBe(400); @@ -441,10 +452,12 @@ describe('create', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.create(postCase).catch((e) => { + return caseClient.client.create(postCase).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); }); diff --git a/x-pack/plugins/case/server/client/cases/create.ts b/x-pack/plugins/case/server/client/cases/create.ts index ee47c59072fdd..f88924483e0b8 100644 --- a/x-pack/plugins/case/server/client/cases/create.ts +++ b/x-pack/plugins/case/server/client/cases/create.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; import { @@ -34,6 +34,7 @@ import { CaseServiceSetup, CaseUserActionServiceSetup, } from '../../services'; +import { createCaseError } from '../../common/error'; interface CreateCaseArgs { caseConfigureService: CaseConfigureServiceSetup; @@ -42,8 +43,12 @@ interface CreateCaseArgs { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; theCase: CasePostRequest; + logger: Logger; } +/** + * Creates a new case. + */ export const create = async ({ savedObjectsClient, caseService, @@ -51,6 +56,7 @@ export const create = async ({ userActionService, user, theCase, + logger, }: CreateCaseArgs): Promise => { // default to an individual case if the type is not defined. const { type = CaseType.individual, ...nonTypeCaseFields } = theCase; @@ -60,41 +66,45 @@ export const create = async ({ fold(throwErrors(Boom.badRequest), identity) ); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); - const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - - const newCase = await caseService.postNewCase({ - client: savedObjectsClient, - attributes: transformNewCase({ - createdDate, - newCase: query, - username, - full_name, - email, - connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), - }), - }); + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const createdDate = new Date().toISOString(); + const myCaseConfigure = await caseConfigureService.find({ client: savedObjectsClient }); + const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCaseUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: newCase.id, - fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], - newValue: JSON.stringify(query), + const newCase = await caseService.postNewCase({ + client: savedObjectsClient, + attributes: transformNewCase({ + createdDate, + newCase: query, + username, + full_name, + email, + connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), }), - ], - }); + }); - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: newCase, - }) - ); + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCaseUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: newCase.id, + fields: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + newValue: JSON.stringify(query), + }), + ], + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: newCase, + }) + ); + } catch (error) { + throw createCaseError({ message: `Failed to create case: ${error}`, error, logger }); + } }; diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts index ab0b97abbcb76..fa556986ee8d3 100644 --- a/x-pack/plugins/case/server/client/cases/get.ts +++ b/x-pack/plugins/case/server/client/cases/get.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; import { CaseResponseRt, CaseResponse } from '../../../common/api'; import { CaseServiceSetup } from '../../services'; import { countAlertsForID } from '../../common'; +import { createCaseError } from '../../common/error'; interface GetParams { savedObjectsClient: SavedObjectsClientContract; @@ -17,50 +18,59 @@ interface GetParams { id: string; includeComments?: boolean; includeSubCaseComments?: boolean; + logger: Logger; } +/** + * Retrieves a case and optionally its comments and sub case comments. + */ export const get = async ({ savedObjectsClient, caseService, id, + logger, includeComments = false, includeSubCaseComments = false, }: GetParams): Promise => { - const [theCase, subCasesForCaseId] = await Promise.all([ - caseService.getCase({ + try { + const [theCase, subCasesForCaseId] = await Promise.all([ + caseService.getCase({ + client: savedObjectsClient, + id, + }), + caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + ]); + + const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); + + if (!includeComments) { + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + subCaseIds, + }) + ); + } + const theComments = await caseService.getAllCaseComments({ client: savedObjectsClient, id, - }), - caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), - ]); + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + includeSubCaseComments, + }); - const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); - - if (!includeComments) { return CaseResponseRt.encode( flattenCaseSavedObject({ savedObject: theCase, + comments: theComments.saved_objects, subCaseIds, + totalComment: theComments.total, + totalAlerts: countAlertsForID({ comments: theComments, id }), }) ); + } catch (error) { + throw createCaseError({ message: `Failed to get case id: ${id}: ${error}`, error, logger }); } - const theComments = await caseService.getAllCaseComments({ - client: savedObjectsClient, - id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - includeSubCaseComments, - }); - - return CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - comments: theComments.saved_objects, - subCaseIds, - totalComment: theComments.total, - totalAlerts: countAlertsForID({ comments: theComments, id }), - }) - ); }; diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts index 1e0c246855d88..352328ed1dd40 100644 --- a/x-pack/plugins/case/server/client/cases/push.ts +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -5,11 +5,12 @@ * 2.0. */ -import Boom, { isBoom, Boom as BoomType } from '@hapi/boom'; +import Boom from '@hapi/boom'; import { SavedObjectsBulkUpdateResponse, SavedObjectsClientContract, SavedObjectsUpdateResponse, + Logger, } from 'kibana/server'; import { ActionResult, ActionsClient } from '../../../../actions/server'; import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils'; @@ -34,16 +35,7 @@ import { CaseUserActionServiceSetup, } from '../../services'; import { CaseClientHandler } from '../client'; - -const createError = (e: Error | BoomType, message: string): Error | BoomType => { - if (isBoom(e)) { - e.message = message; - e.output.payload.message = message; - return e; - } - - return Error(message); -}; +import { createCaseError } from '../../common/error'; interface PushParams { savedObjectsClient: SavedObjectsClientContract; @@ -55,6 +47,7 @@ interface PushParams { connectorId: string; caseClient: CaseClientHandler; actionsClient: ActionsClient; + logger: Logger; } export const push = async ({ @@ -67,6 +60,7 @@ export const push = async ({ connectorId, caseId, user, + logger, }: PushParams): Promise => { /* Start of push to external service */ let theCase: CaseResponse; @@ -84,7 +78,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error getting case and/or connector and/or user actions: ${e.message}`; - throw createError(e, message); + throw createCaseError({ message, error: e, logger }); } // We need to change the logic when we support subcases @@ -102,7 +96,11 @@ export const push = async ({ indices, }); } catch (e) { - throw new Error(`Error getting alerts for case with id ${theCase.id}: ${e.message}`); + throw createCaseError({ + message: `Error getting alerts for case with id ${theCase.id}: ${e.message}`, + logger, + error: e, + }); } try { @@ -113,7 +111,7 @@ export const push = async ({ }); } catch (e) { const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; - throw createError(e, message); + throw createCaseError({ message, error: e, logger }); } try { @@ -127,7 +125,7 @@ export const push = async ({ }); } catch (e) { const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } const pushRes = await actionsClient.execute({ @@ -171,7 +169,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -257,7 +255,7 @@ export const push = async ({ ]); } catch (e) { const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; - throw createError(e, message); + throw createCaseError({ error: e, message, logger }); } /* End of update case with push information */ diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts index 2dd2caf9fe73a..f1d56e7132bd1 100644 --- a/x-pack/plugins/case/server/client/cases/types.ts +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -72,7 +72,7 @@ export interface TransformFieldsArgs { export interface ExternalServiceComment { comment: string; - commentId?: string; + commentId: string; } export interface MapIncident { diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 7a3e4458f25c5..752b0ab369de0 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -6,6 +6,7 @@ */ import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, mockCaseNoConnectorId, @@ -640,14 +641,16 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when missing version', async () => { @@ -671,18 +674,20 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client - // @ts-expect-error - .update({ cases: patchCases }) - .catch((e) => { - expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); - }); + return ( + caseClient.client + // @ts-expect-error + .update({ cases: patchCases }) + .catch((e) => { + expect(e).not.toBeNull(); + expect(e.isBoom).toBe(true); + expect(e.output.statusCode).toBe(400); + }) + ); }); test('it throws when fields are identical', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -698,16 +703,18 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(406); - expect(e.message).toBe('All update fields are identical to current version.'); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(406); + expect(boomErr.message).toContain('All update fields are identical to current version.'); }); }); test('it throws when case does not exist', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -728,18 +735,20 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(404); - expect(e.message).toBe( + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(404); + expect(boomErr.message).toContain( 'These cases not-exists do not exist. Please check you have the correct ids.' ); }); }); test('it throws when cases conflicts', async () => { - expect.assertions(4); + expect.assertions(5); const patchCases = { cases: [ { @@ -755,11 +764,13 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.update(patchCases).catch((e) => { + return caseClient.client.update(patchCases).catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(409); - expect(e.message).toBe( + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(409); + expect(boomErr.message).toContain( 'These cases mock-id-1 has been updated. Please refresh before saving additional updates.' ); }); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index a4ca2b4cbdef9..36318f03bd33f 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -15,6 +15,7 @@ import { SavedObjectsClientContract, SavedObjectsFindResponse, SavedObjectsFindResult, + Logger, } from 'kibana/server'; import { AlertInfo, @@ -53,6 +54,7 @@ import { } from '../../saved_object_types'; import { CaseClientHandler } from '..'; import { addAlertInfoToStatusMap } from '../../common'; +import { createCaseError } from '../../common/error'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. @@ -325,6 +327,7 @@ interface UpdateArgs { user: User; caseClient: CaseClientHandler; cases: CasesPatchRequest; + logger: Logger; } export const update = async ({ @@ -334,175 +337,189 @@ export const update = async ({ user, caseClient, cases, + logger, }: UpdateArgs): Promise => { const query = pipe( excess(CasesPatchRequestRt).decode(cases), fold(throwErrors(Boom.badRequest), identity) ); - const myCases = await caseService.getCases({ - client: savedObjectsClient, - caseIds: query.cases.map((q) => q.id), - }); - - let nonExistingCases: CasePatchRequest[] = []; - const conflictedCases = query.cases.filter((q) => { - const myCase = myCases.saved_objects.find((c) => c.id === q.id); + try { + const myCases = await caseService.getCases({ + client: savedObjectsClient, + caseIds: query.cases.map((q) => q.id), + }); - if (myCase && myCase.error) { - nonExistingCases = [...nonExistingCases, q]; - return false; - } - return myCase == null || myCase?.version !== q.version; - }); + let nonExistingCases: CasePatchRequest[] = []; + const conflictedCases = query.cases.filter((q) => { + const myCase = myCases.saved_objects.find((c) => c.id === q.id); - if (nonExistingCases.length > 0) { - throw Boom.notFound( - `These cases ${nonExistingCases - .map((c) => c.id) - .join(', ')} do not exist. Please check you have the correct ids.` - ); - } + if (myCase && myCase.error) { + nonExistingCases = [...nonExistingCases, q]; + return false; + } + return myCase == null || myCase?.version !== q.version; + }); - if (conflictedCases.length > 0) { - throw Boom.conflict( - `These cases ${conflictedCases - .map((c) => c.id) - .join(', ')} has been updated. Please refresh before saving additional updates.` - ); - } + if (nonExistingCases.length > 0) { + throw Boom.notFound( + `These cases ${nonExistingCases + .map((c) => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } - const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { - const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { connector, ...thisCase } = updateCase; - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...thisCase, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - }) - : { id: thisCase.id, version: thisCase.version }; - }); + if (conflictedCases.length > 0) { + throw Boom.conflict( + `These cases ${conflictedCases + .map((c) => c.id) + .join(', ')} has been updated. Please refresh before saving additional updates.` + ); + } - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; - }); + const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { + const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); + const { connector, ...thisCase } = updateCase; + return currentCase != null + ? getCaseToUpdate(currentCase.attributes, { + ...thisCase, + ...(connector != null + ? { connector: transformCaseConnectorToEsConnector(connector) } + : {}), + }) + : { id: thisCase.id, version: thisCase.version }; + }); - if (updateFilterCases.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } + const updateFilterCases = updateCases.filter((updateCase) => { + const { id, version, ...updateCaseAttributes } = updateCase; + return Object.keys(updateCaseAttributes).length > 0; + }); - const casesMap = myCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); + if (updateFilterCases.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } - throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); - throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); - await throwIfInvalidUpdateOfTypeWithAlerts({ - requests: updateFilterCases, - caseService, - client: savedObjectsClient, - }); + const casesMap = myCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); + + throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); + throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); + await throwIfInvalidUpdateOfTypeWithAlerts({ + requests: updateFilterCases, + caseService, + client: savedObjectsClient, + }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const updatedDt = new Date().toISOString(); - const updatedCases = await caseService.patchCases({ - client: savedObjectsClient, - cases: updateFilterCases.map((thisCase) => { - const { id: caseId, version, ...updateCaseAttributes } = thisCase; - let closedInfo = {}; - if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { - closedInfo = { - closed_at: updatedDt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateCaseAttributes.status && - (updateCaseAttributes.status === CaseStatuses.open || - updateCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { - closed_at: null, - closed_by: null, + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const updatedDt = new Date().toISOString(); + const updatedCases = await caseService.patchCases({ + client: savedObjectsClient, + cases: updateFilterCases.map((thisCase) => { + const { id: caseId, version, ...updateCaseAttributes } = thisCase; + let closedInfo = {}; + if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { + closedInfo = { + closed_at: updatedDt, + closed_by: { email, full_name, username }, + }; + } else if ( + updateCaseAttributes.status && + (updateCaseAttributes.status === CaseStatuses.open || + updateCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + caseId, + updatedAttributes: { + ...updateCaseAttributes, + ...closedInfo, + updated_at: updatedDt, + updated_by: { email, full_name, username }, + }, + version, }; - } - return { - caseId, - updatedAttributes: { - ...updateCaseAttributes, - ...closedInfo, - updated_at: updatedDt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); + }), + }); - // If a status update occurred and the case is synced then we need to update all alerts' status - // attached to the case to the new status. - const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); - return ( - currentCase != null && - caseToUpdate.status != null && - currentCase.attributes.status !== caseToUpdate.status && - currentCase.attributes.settings.syncAlerts - ); - }); + // If a status update occurred and the case is synced then we need to update all alerts' status + // attached to the case to the new status. + const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.status != null && + currentCase.attributes.status !== caseToUpdate.status && + currentCase.attributes.settings.syncAlerts + ); + }); - // If syncAlerts setting turned on we need to update all alerts' status - // attached to the case to the current status. - const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); - return ( - currentCase != null && - caseToUpdate.settings?.syncAlerts != null && - currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && - caseToUpdate.settings.syncAlerts - ); - }); + // If syncAlerts setting turned on we need to update all alerts' status + // attached to the case to the current status. + const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { + const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + return ( + currentCase != null && + caseToUpdate.settings?.syncAlerts != null && + currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && + caseToUpdate.settings.syncAlerts + ); + }); - // Update the alert's status to match any case status or sync settings changes - await updateAlerts({ - casesWithStatusChangedAndSynced, - casesWithSyncSettingChangedToOn, - caseService, - client: savedObjectsClient, - caseClient, - casesMap, - }); + // Update the alert's status to match any case status or sync settings changes + await updateAlerts({ + casesWithStatusChangedAndSynced, + casesWithSyncSettingChangedToOn, + caseService, + client: savedObjectsClient, + caseClient, + casesMap, + }); - const returnUpdatedCase = myCases.saved_objects - .filter((myCase) => - updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) - ) - .map((myCase) => { - const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); - return flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - version: updatedCase?.version ?? myCase.version, - }, + const returnUpdatedCase = myCases.saved_objects + .filter((myCase) => + updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) + ) + .map((myCase) => { + const updatedCase = updatedCases.saved_objects.find((c) => c.id === myCase.id); + return flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + version: updatedCase?.version ?? myCase.version, + }, + }); }); - }); - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: buildCaseUserActions({ - originalCases: myCases.saved_objects, - updatedCases: updatedCases.saved_objects, - actionDate: updatedDt, - actionBy: { email, full_name, username }, - }), - }); + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: buildCaseUserActions({ + originalCases: myCases.saved_objects, + updatedCases: updatedCases.saved_objects, + actionDate: updatedDt, + actionBy: { email, full_name, username }, + }), + }); - return CasesResponseRt.encode(returnUpdatedCase); + return CasesResponseRt.encode(returnUpdatedCase); + } catch (error) { + const idVersions = cases.cases.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + })); + + throw createCaseError({ + message: `Failed to update case, ids: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts index 44e7a682aa7ed..859114a5e8fb0 100644 --- a/x-pack/plugins/case/server/client/cases/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -540,6 +540,7 @@ describe('utils', () => { }, { comment: 'Elastic Security Alerts attached to the case: 3', + commentId: 'mock-id-1-total-alerts', }, ]); }); @@ -569,6 +570,7 @@ describe('utils', () => { }, { comment: 'Elastic Security Alerts attached to the case: 4', + commentId: 'mock-id-1-total-alerts', }, ]); }); diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts index a5013d9b93982..67d5ef55f83c3 100644 --- a/x-pack/plugins/case/server/client/cases/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -185,6 +185,7 @@ export const createIncident = async ({ if (totalAlerts > 0) { comments.push({ comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`, + commentId: `${theCase.id}-total-alerts`, }); } diff --git a/x-pack/plugins/case/server/client/client.ts b/x-pack/plugins/case/server/client/client.ts index c684548decbe6..c34c3942b18d0 100644 --- a/x-pack/plugins/case/server/client/client.ts +++ b/x-pack/plugins/case/server/client/client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; import { CaseClientFactoryArguments, CaseClient, @@ -36,6 +36,7 @@ import { get } from './cases/get'; import { get as getUserActions } from './user_actions/get'; import { get as getAlerts } from './alerts/get'; import { push } from './cases/push'; +import { createCaseError } from '../common/error'; /** * This class is a pass through for common case functionality (like creating, get a case). @@ -49,6 +50,7 @@ export class CaseClientHandler implements CaseClient { private readonly _savedObjectsClient: SavedObjectsClientContract; private readonly _userActionService: CaseUserActionServiceSetup; private readonly _alertsService: AlertServiceContract; + private readonly logger: Logger; constructor(clientArgs: CaseClientFactoryArguments) { this._scopedClusterClient = clientArgs.scopedClusterClient; @@ -59,96 +61,190 @@ export class CaseClientHandler implements CaseClient { this._savedObjectsClient = clientArgs.savedObjectsClient; this._userActionService = clientArgs.userActionService; this._alertsService = clientArgs.alertsService; + this.logger = clientArgs.logger; } public async create(caseInfo: CasePostRequest) { - return create({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - caseConfigureService: this._caseConfigureService, - userActionService: this._userActionService, - user: this.user, - theCase: caseInfo, - }); + try { + return create({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + caseConfigureService: this._caseConfigureService, + userActionService: this._userActionService, + user: this.user, + theCase: caseInfo, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a new case using client: ${error}`, + error, + logger: this.logger, + }); + } } public async update(cases: CasesPatchRequest) { - return update({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - cases, - caseClient: this, - }); + try { + return update({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + user: this.user, + cases, + caseClient: this, + logger: this.logger, + }); + } catch (error) { + const caseIDVersions = cases.cases.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + })); + throw createCaseError({ + message: `Failed to update cases using client: ${JSON.stringify(caseIDVersions)}: ${error}`, + error, + logger: this.logger, + }); + } } public async addComment({ caseId, comment }: CaseClientAddComment) { - return addComment({ - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - caseClient: this, - caseId, - comment, - user: this.user, - }); + try { + return addComment({ + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + caseClient: this, + caseId, + comment, + user: this.user, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to add comment using client case id: ${caseId}: ${error}`, + error, + logger: this.logger, + }); + } } public async getFields(fields: ConfigureFields) { - return getFields(fields); + try { + return getFields(fields); + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve fields using client: ${error}`, + error, + logger: this.logger, + }); + } } public async getMappings(args: MappingsClient) { - return getMappings({ - ...args, - savedObjectsClient: this._savedObjectsClient, - connectorMappingsService: this._connectorMappingsService, - caseClient: this, - }); + try { + return getMappings({ + ...args, + savedObjectsClient: this._savedObjectsClient, + connectorMappingsService: this._connectorMappingsService, + caseClient: this, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get mappings using client: ${error}`, + error, + logger: this.logger, + }); + } } public async updateAlertsStatus(args: CaseClientUpdateAlertsStatus) { - return updateAlertsStatus({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - }); + try { + return updateAlertsStatus({ + ...args, + alertsService: this._alertsService, + scopedClusterClient: this._scopedClusterClient, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to update alerts status using client ids: ${JSON.stringify( + args.ids + )} \nindices: ${JSON.stringify([...args.indices])} \nstatus: ${args.status}: ${error}`, + error, + logger: this.logger, + }); + } } public async get(args: CaseClientGet) { - return get({ - ...args, - caseService: this._caseService, - savedObjectsClient: this._savedObjectsClient, - }); + try { + return get({ + ...args, + caseService: this._caseService, + savedObjectsClient: this._savedObjectsClient, + logger: this.logger, + }); + } catch (error) { + this.logger.error(`Failed to get case using client id: ${args.id}: ${error}`); + throw error; + } } public async getUserActions(args: CaseClientGetUserActions) { - return getUserActions({ - ...args, - savedObjectsClient: this._savedObjectsClient, - userActionService: this._userActionService, - }); + try { + return getUserActions({ + ...args, + savedObjectsClient: this._savedObjectsClient, + userActionService: this._userActionService, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get user actions using client id: ${args.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } public async getAlerts(args: CaseClientGetAlerts) { - return getAlerts({ - ...args, - alertsService: this._alertsService, - scopedClusterClient: this._scopedClusterClient, - }); + try { + return getAlerts({ + ...args, + alertsService: this._alertsService, + scopedClusterClient: this._scopedClusterClient, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to get alerts using client ids: ${JSON.stringify( + args.ids + )} \nindices: ${JSON.stringify([...args.indices])}: ${error}`, + error, + logger: this.logger, + }); + } } public async push(args: CaseClientPush) { - return push({ - ...args, - savedObjectsClient: this._savedObjectsClient, - caseService: this._caseService, - userActionService: this._userActionService, - user: this.user, - caseClient: this, - caseConfigureService: this._caseConfigureService, - }); + try { + return push({ + ...args, + savedObjectsClient: this._savedObjectsClient, + caseService: this._caseService, + userActionService: this._userActionService, + user: this.user, + caseClient: this, + caseConfigureService: this._caseConfigureService, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to push case using client id: ${args.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } } diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index c9b1e4fd13272..123ecec6abea3 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash/fp'; import { CommentType } from '../../../common/api'; +import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, mockCaseComments, @@ -297,7 +298,7 @@ describe('addComment', () => { caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -326,7 +327,7 @@ describe('addComment', () => { ['comment'].forEach((attribute) => { const requestAttributes = omit(attribute, allRequestAttributes); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -353,7 +354,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['alertId', 'index'].forEach((attribute) => { - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -387,7 +388,7 @@ describe('addComment', () => { ['alertId', 'index'].forEach((attribute) => { const requestAttributes = omit(attribute, allRequestAttributes); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', // @ts-expect-error @@ -414,7 +415,7 @@ describe('addComment', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); ['comment'].forEach((attribute) => { - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -437,14 +438,14 @@ describe('addComment', () => { }); test('it throws when the case does not exists', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'not-exists', comment: { @@ -454,20 +455,22 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(404); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(404); }); }); test('it throws when postNewCase throws', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-1', comment: { @@ -477,13 +480,15 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); test('it throws when the case is closed and the comment is of type alert', async () => { - expect.assertions(3); + expect.assertions(4); const savedObjectsClient = createMockSavedObjectsRepository({ caseSavedObject: mockCases, @@ -491,7 +496,7 @@ describe('addComment', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client + return caseClient.client .addComment({ caseId: 'mock-id-4', comment: { @@ -506,8 +511,10 @@ describe('addComment', () => { }) .catch((e) => { expect(e).not.toBeNull(); - expect(e.isBoom).toBe(true); - expect(e.output.statusCode).toBe(400); + expect(isCaseError(e)).toBeTruthy(); + const boomErr = e.boomify(); + expect(boomErr.isBoom).toBe(true); + expect(boomErr.output.statusCode).toBe(400); }); }); }); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 0a86c1825fedc..d3d7047e71bd3 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; import { decodeCommentRequest, getAlertIds, @@ -25,7 +25,7 @@ import { CaseType, SubCaseAttributes, CommentRequest, - CollectionWithSubCaseResponse, + CaseResponse, User, CommentRequestAlertType, AlertCommentRequestRt, @@ -38,6 +38,7 @@ import { import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; import { CommentableCase } from '../../common'; import { CaseClientHandler } from '..'; +import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; @@ -104,6 +105,7 @@ interface AddCommentFromRuleArgs { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; + logger: Logger; } const addGeneratedAlerts = async ({ @@ -113,7 +115,8 @@ const addGeneratedAlerts = async ({ caseClient, caseId, comment, -}: AddCommentFromRuleArgs): Promise => { + logger, +}: AddCommentFromRuleArgs): Promise => { const query = pipe( AlertCommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -125,88 +128,104 @@ const addGeneratedAlerts = async ({ if (comment.type !== CommentType.generatedAlert) { throw Boom.internal('Attempting to add a non generated alert in the wrong context'); } - const createdDate = new Date().toISOString(); - const caseInfo = await caseService.getCase({ - client: savedObjectsClient, - id: caseId, - }); + try { + const createdDate = new Date().toISOString(); - if ( - query.type === CommentType.generatedAlert && - caseInfo.attributes.type !== CaseType.collection - ) { - throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case'); - } + const caseInfo = await caseService.getCase({ + client: savedObjectsClient, + id: caseId, + }); - const userDetails: User = { - username: caseInfo.attributes.created_by?.username, - full_name: caseInfo.attributes.created_by?.full_name, - email: caseInfo.attributes.created_by?.email, - }; + if ( + query.type === CommentType.generatedAlert && + caseInfo.attributes.type !== CaseType.collection + ) { + throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case'); + } - const subCase = await getSubCase({ - caseService, - savedObjectsClient, - caseId, - createdAt: createdDate, - userActionService, - user: userDetails, - }); + const userDetails: User = { + username: caseInfo.attributes.created_by?.username, + full_name: caseInfo.attributes.created_by?.full_name, + email: caseInfo.attributes.created_by?.email, + }; - const commentableCase = new CommentableCase({ - collection: caseInfo, - subCase, - soClient: savedObjectsClient, - service: caseService, - }); + const subCase = await getSubCase({ + caseService, + savedObjectsClient, + caseId, + createdAt: createdDate, + userActionService, + user: userDetails, + }); - const { - comment: newComment, - commentableCase: updatedCase, - } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); - - if ( - (newComment.attributes.type === CommentType.alert || - newComment.attributes.type === CommentType.generatedAlert) && - caseInfo.attributes.settings.syncAlerts - ) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, - status: subCase.attributes.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + const commentableCase = new CommentableCase({ + logger, + collection: caseInfo, + subCase, + soClient: savedObjectsClient, + service: caseService, }); - } - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { ...userDetails }, - caseId: updatedCase.caseId, - subCaseId: updatedCase.subCaseId, - commentId: newComment.id, - fields: ['comment'], - newValue: JSON.stringify(query), - }), - ], - }); + const { + comment: newComment, + commentableCase: updatedCase, + } = await commentableCase.createComment({ createdDate, user: userDetails, commentReq: query }); + + if ( + (newComment.attributes.type === CommentType.alert || + newComment.attributes.type === CommentType.generatedAlert) && + caseInfo.attributes.settings.syncAlerts + ) { + const ids = getAlertIds(query); + await caseClient.updateAlertsStatus({ + ids, + status: subCase.attributes.status, + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), + }); + } + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { ...userDetails }, + caseId: updatedCase.caseId, + subCaseId: updatedCase.subCaseId, + commentId: newComment.id, + fields: ['comment'], + newValue: JSON.stringify(query), + }), + ], + }); - return updatedCase.encode(); + return updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed while adding a generated alert to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } }; -async function getCombinedCase( - service: CaseServiceSetup, - client: SavedObjectsClientContract, - id: string -): Promise { +async function getCombinedCase({ + service, + client, + id, + logger, +}: { + service: CaseServiceSetup; + client: SavedObjectsClientContract; + id: string; + logger: Logger; +}): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ service.getCase({ client, @@ -225,6 +244,7 @@ async function getCombinedCase( id: subCasePromise.value.references[0].id, }); return new CommentableCase({ + logger, collection: caseValue, subCase: subCasePromise.value, service, @@ -238,7 +258,12 @@ async function getCombinedCase( if (casePromise.status === 'rejected') { throw casePromise.reason; } else { - return new CommentableCase({ collection: casePromise.value, service, soClient: client }); + return new CommentableCase({ + logger, + collection: casePromise.value, + service, + soClient: client, + }); } } @@ -250,6 +275,7 @@ interface AddCommentArgs { caseService: CaseServiceSetup; userActionService: CaseUserActionServiceSetup; user: User; + logger: Logger; } export const addComment = async ({ @@ -260,7 +286,8 @@ export const addComment = async ({ caseId, comment, user, -}: AddCommentArgs): Promise => { + logger, +}: AddCommentArgs): Promise => { const query = pipe( CommentRequestRt.decode(comment), fold(throwErrors(Boom.badRequest), identity) @@ -274,56 +301,70 @@ export const addComment = async ({ savedObjectsClient, userActionService, caseService, + logger, }); } decodeCommentRequest(comment); - const createdDate = new Date().toISOString(); - - const combinedCase = await getCombinedCase(caseService, savedObjectsClient, caseId); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = user; - const userInfo: User = { - username, - full_name, - email, - }; - - const { comment: newComment, commentableCase: updatedCase } = await combinedCase.createComment({ - createdDate, - user: userInfo, - commentReq: query, - }); + try { + const createdDate = new Date().toISOString(); - if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, - status: updatedCase.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + const combinedCase = await getCombinedCase({ + service: caseService, + client: savedObjectsClient, + id: caseId, + logger, }); - } - await userActionService.postUserActions({ - client: savedObjectsClient, - actions: [ - buildCommentUserActionItem({ - action: 'create', - actionAt: createdDate, - actionBy: { username, full_name, email }, - caseId: updatedCase.caseId, - subCaseId: updatedCase.subCaseId, - commentId: newComment.id, - fields: ['comment'], - newValue: JSON.stringify(query), - }), - ], - }); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const userInfo: User = { + username, + full_name, + email, + }; + + const { comment: newComment, commentableCase: updatedCase } = await combinedCase.createComment({ + createdDate, + user: userInfo, + commentReq: query, + }); + + if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { + const ids = getAlertIds(query); + await caseClient.updateAlertsStatus({ + ids, + status: updatedCase.status, + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), + }); + } + + await userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + buildCommentUserActionItem({ + action: 'create', + actionAt: createdDate, + actionBy: { username, full_name, email }, + caseId: updatedCase.caseId, + subCaseId: updatedCase.subCaseId, + commentId: newComment.id, + fields: ['comment'], + newValue: JSON.stringify(query), + }), + ], + }); - return updatedCase.encode(); + return updatedCase.encode(); + } catch (error) { + throw createCaseError({ + message: `Failed while adding a comment to case id: ${caseId} error: ${error}`, + error, + logger, + }); + } }; diff --git a/x-pack/plugins/case/server/client/configure/get_mappings.ts b/x-pack/plugins/case/server/client/configure/get_mappings.ts index 5dd90efd8a2d7..5553580a41560 100644 --- a/x-pack/plugins/case/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/case/server/client/configure/get_mappings.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { ActionsClient } from '../../../../actions/server'; import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; import { ConnectorMappingsServiceSetup } from '../../services'; import { CaseClientHandler } from '..'; +import { createCaseError } from '../../common/error'; interface GetMappingsArgs { savedObjectsClient: SavedObjectsClientContract; @@ -20,6 +21,7 @@ interface GetMappingsArgs { caseClient: CaseClientHandler; connectorType: string; connectorId: string; + logger: Logger; } export const getMappings = async ({ @@ -29,42 +31,51 @@ export const getMappings = async ({ caseClient, connectorType, connectorId, + logger, }: GetMappingsArgs): Promise => { - if (connectorType === ConnectorTypes.none) { - return []; - } - const myConnectorMappings = await connectorMappingsService.find({ - client: savedObjectsClient, - options: { - hasReference: { - type: ACTION_SAVED_OBJECT_TYPE, - id: connectorId, - }, - }, - }); - let theMapping; - // Create connector mappings if there are none - if (myConnectorMappings.total === 0) { - const res = await caseClient.getFields({ - actionsClient, - connectorId, - connectorType, - }); - theMapping = await connectorMappingsService.post({ + try { + if (connectorType === ConnectorTypes.none) { + return []; + } + const myConnectorMappings = await connectorMappingsService.find({ client: savedObjectsClient, - attributes: { - mappings: res.defaultMappings, - }, - references: [ - { + options: { + hasReference: { type: ACTION_SAVED_OBJECT_TYPE, - name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, id: connectorId, }, - ], + }, + }); + let theMapping; + // Create connector mappings if there are none + if (myConnectorMappings.total === 0) { + const res = await caseClient.getFields({ + actionsClient, + connectorId, + connectorType, + }); + theMapping = await connectorMappingsService.post({ + client: savedObjectsClient, + attributes: { + mappings: res.defaultMappings, + }, + references: [ + { + type: ACTION_SAVED_OBJECT_TYPE, + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + id: connectorId, + }, + ], + }); + } else { + theMapping = myConnectorMappings.saved_objects[0]; + } + return theMapping ? theMapping.attributes.mappings : []; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + error, + logger, }); - } else { - theMapping = myConnectorMappings.saved_objects[0]; } - return theMapping ? theMapping.attributes.mappings : []; }; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 8a085bf29f214..6f4b4b136f68f 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, + loggingSystemMock, savedObjectsClientMock, } from '../../../../../src/core/server/mocks'; import { nullUser } from '../common'; @@ -22,6 +23,7 @@ jest.mock('./client'); import { CaseClientHandler } from './client'; import { createExternalCaseClient } from './index'; +const logger = loggingSystemMock.create().get('case'); const esClient = elasticsearchServiceMock.createElasticsearchClient(); const caseConfigureService = createConfigureServiceMock(); const alertsService = createAlertServiceMock(); @@ -41,6 +43,7 @@ describe('createExternalCaseClient()', () => { user: nullUser, savedObjectsClient, userActionService, + logger, }); expect(CaseClientHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 302745913babb..98ffed0eaf8c5 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -47,7 +47,6 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ }; }> => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); - // const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const auth = badAuth ? authenticationMock.createInvalid() : authenticationMock.create(); @@ -78,6 +77,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ userActionService, alertsService, scopedClusterClient: esClient, + logger: log, }); return { client: caseClient, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index ba5677426c222..adc66d8b1ea77 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { CasePostRequest, @@ -13,7 +13,6 @@ import { CasesPatchRequest, CasesResponse, CaseStatuses, - CollectionWithSubCaseResponse, CommentRequest, ConnectorMappingsAttributes, GetFieldsResponse, @@ -77,6 +76,7 @@ export interface CaseClientFactoryArguments { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; + logger: Logger; } export interface ConfigureFields { @@ -89,7 +89,7 @@ export interface ConfigureFields { * This represents the interface that other plugins can access. */ export interface CaseClient { - addComment(args: CaseClientAddComment): Promise; + addComment(args: CaseClientAddComment): Promise; create(theCase: CasePostRequest): Promise; get(args: CaseClientGet): Promise; getAlerts(args: CaseClientGetAlerts): Promise; diff --git a/x-pack/plugins/case/server/common/error.ts b/x-pack/plugins/case/server/common/error.ts new file mode 100644 index 0000000000000..95b05fd612e60 --- /dev/null +++ b/x-pack/plugins/case/server/common/error.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Boom, isBoom } from '@hapi/boom'; +import { Logger } from 'src/core/server'; + +/** + * Helper class for wrapping errors while preserving the original thrown error. + */ +class CaseError extends Error { + public readonly wrappedError?: Error; + constructor(message?: string, originalError?: Error) { + super(message); + this.name = this.constructor.name; // for stack traces + if (isCaseError(originalError)) { + this.wrappedError = originalError.wrappedError; + } else { + this.wrappedError = originalError; + } + } + + /** + * This function creates a boom representation of the error. If the wrapped error is a boom we'll grab the statusCode + * and data from that. + */ + public boomify(): Boom { + const message = this.message ?? this.wrappedError?.message; + let statusCode = 500; + let data: unknown | undefined; + + if (isBoom(this.wrappedError)) { + data = this.wrappedError?.data; + statusCode = this.wrappedError?.output?.statusCode ?? 500; + } + + return new Boom(message, { + data, + statusCode, + }); + } +} + +/** + * Type guard for determining if an error is a CaseError + */ +export function isCaseError(error: unknown): error is CaseError { + return error instanceof CaseError; +} + +/** + * Create a CaseError that wraps the original thrown error. This also logs the message that will be placed in the CaseError + * if the logger was defined. + */ +export function createCaseError({ + message, + error, + logger, +}: { + message?: string; + error?: Error; + logger?: Logger; +}) { + const logMessage: string | undefined = message ?? error?.toString(); + if (logMessage !== undefined) { + logger?.error(logMessage); + } + + return new CaseError(message, error); +} diff --git a/x-pack/plugins/case/server/common/models/commentable_case.ts b/x-pack/plugins/case/server/common/models/commentable_case.ts index 9827118ee8e29..1ff5b7beadcaf 100644 --- a/x-pack/plugins/case/server/common/models/commentable_case.ts +++ b/x-pack/plugins/case/server/common/models/commentable_case.ts @@ -11,14 +11,15 @@ import { SavedObjectReference, SavedObjectsClientContract, SavedObjectsUpdateResponse, + Logger, } from 'src/core/server'; import { AssociationType, CaseSettings, CaseStatuses, CaseType, - CollectionWithSubCaseResponse, - CollectWithSubCaseResponseRt, + CaseResponse, + CaseResponseRt, CommentAttributes, CommentPatchRequest, CommentRequest, @@ -35,6 +36,7 @@ import { } from '../../routes/api/utils'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../saved_object_types'; import { CaseServiceSetup } from '../../services'; +import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; interface UpdateCommentResp { @@ -52,6 +54,7 @@ interface CommentableCaseParams { subCase?: SavedObject; soClient: SavedObjectsClientContract; service: CaseServiceSetup; + logger: Logger; } /** @@ -63,11 +66,14 @@ export class CommentableCase { private readonly subCase?: SavedObject; private readonly soClient: SavedObjectsClientContract; private readonly service: CaseServiceSetup; - constructor({ collection, subCase, soClient, service }: CommentableCaseParams) { + private readonly logger: Logger; + + constructor({ collection, subCase, soClient, service, logger }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; this.soClient = soClient; this.service = service; + this.logger = logger; } public get status(): CaseStatuses { @@ -119,55 +125,64 @@ export class CommentableCase { } private async update({ date, user }: { date: string; user: User }): Promise { - let updatedSubCaseAttributes: SavedObject | undefined; + try { + let updatedSubCaseAttributes: SavedObject | undefined; + + if (this.subCase) { + const updatedSubCase = await this.service.patchSubCase({ + client: this.soClient, + subCaseId: this.subCase.id, + updatedAttributes: { + updated_at: date, + updated_by: { + ...user, + }, + }, + version: this.subCase.version, + }); - if (this.subCase) { - const updatedSubCase = await this.service.patchSubCase({ + updatedSubCaseAttributes = { + ...this.subCase, + attributes: { + ...this.subCase.attributes, + ...updatedSubCase.attributes, + }, + version: updatedSubCase.version ?? this.subCase.version, + }; + } + + const updatedCase = await this.service.patchCase({ client: this.soClient, - subCaseId: this.subCase.id, + caseId: this.collection.id, updatedAttributes: { updated_at: date, - updated_by: { - ...user, - }, + updated_by: { ...user }, }, - version: this.subCase.version, + version: this.collection.version, }); - updatedSubCaseAttributes = { - ...this.subCase, - attributes: { - ...this.subCase.attributes, - ...updatedSubCase.attributes, + // this will contain the updated sub case information if the sub case was defined initially + return new CommentableCase({ + collection: { + ...this.collection, + attributes: { + ...this.collection.attributes, + ...updatedCase.attributes, + }, + version: updatedCase.version ?? this.collection.version, }, - version: updatedSubCase.version ?? this.subCase.version, - }; + subCase: updatedSubCaseAttributes, + soClient: this.soClient, + service: this.service, + logger: this.logger, + }); + } catch (error) { + throw createCaseError({ + message: `Failed to update commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); } - - const updatedCase = await this.service.patchCase({ - client: this.soClient, - caseId: this.collection.id, - updatedAttributes: { - updated_at: date, - updated_by: { ...user }, - }, - version: this.collection.version, - }); - - // this will contain the updated sub case information if the sub case was defined initially - return new CommentableCase({ - collection: { - ...this.collection, - attributes: { - ...this.collection.attributes, - ...updatedCase.attributes, - }, - version: updatedCase.version ?? this.collection.version, - }, - subCase: updatedSubCaseAttributes, - soClient: this.soClient, - service: this.service, - }); } /** @@ -182,25 +197,33 @@ export class CommentableCase { updatedAt: string; user: User; }): Promise { - const { id, version, ...queryRestAttributes } = updateRequest; + try { + const { id, version, ...queryRestAttributes } = updateRequest; - const [comment, commentableCase] = await Promise.all([ - this.service.patchComment({ - client: this.soClient, - commentId: id, - updatedAttributes: { - ...queryRestAttributes, - updated_at: updatedAt, - updated_by: user, - }, - version, - }), - this.update({ date: updatedAt, user }), - ]); - return { - comment, - commentableCase, - }; + const [comment, commentableCase] = await Promise.all([ + this.service.patchComment({ + client: this.soClient, + commentId: id, + updatedAttributes: { + ...queryRestAttributes, + updated_at: updatedAt, + updated_by: user, + }, + version, + }), + this.update({ date: updatedAt, user }), + ]); + return { + comment, + commentableCase, + }; + } catch (error) { + throw createCaseError({ + message: `Failed to update comment in commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } /** @@ -215,33 +238,41 @@ export class CommentableCase { user: User; commentReq: CommentRequest; }): Promise { - if (commentReq.type === CommentType.alert) { - if (this.status === CaseStatuses.closed) { - throw Boom.badRequest('Alert cannot be attached to a closed case'); - } + try { + if (commentReq.type === CommentType.alert) { + if (this.status === CaseStatuses.closed) { + throw Boom.badRequest('Alert cannot be attached to a closed case'); + } - if (!this.subCase && this.collection.attributes.type === CaseType.collection) { - throw Boom.badRequest('Alert cannot be attached to a collection case'); + if (!this.subCase && this.collection.attributes.type === CaseType.collection) { + throw Boom.badRequest('Alert cannot be attached to a collection case'); + } } - } - const [comment, commentableCase] = await Promise.all([ - this.service.postNewComment({ - client: this.soClient, - attributes: transformNewComment({ - associationType: this.subCase ? AssociationType.subCase : AssociationType.case, - createdDate, - ...commentReq, - ...user, + const [comment, commentableCase] = await Promise.all([ + this.service.postNewComment({ + client: this.soClient, + attributes: transformNewComment({ + associationType: this.subCase ? AssociationType.subCase : AssociationType.case, + createdDate, + ...commentReq, + ...user, + }), + references: this.buildRefsToCase(), }), - references: this.buildRefsToCase(), - }), - this.update({ date: createdDate, user }), - ]); - return { - comment, - commentableCase, - }; + this.update({ date: createdDate, user }), + ]); + return { + comment, + commentableCase, + }; + } catch (error) { + throw createCaseError({ + message: `Failed creating a comment on a commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } private formatCollectionForEncoding(totalComment: number) { @@ -254,47 +285,75 @@ export class CommentableCase { }; } - public async encode(): Promise { - const collectionCommentStats = await this.service.getAllCaseComments({ - client: this.soClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }); + public async encode(): Promise { + try { + const collectionCommentStats = await this.service.getAllCaseComments({ + client: this.soClient, + id: this.collection.id, + options: { + fields: [], + page: 1, + perPage: 1, + }, + }); - if (this.subCase) { - const subCaseComments = await this.service.getAllSubCaseComments({ + const collectionComments = await this.service.getAllCaseComments({ client: this.soClient, - id: this.subCase.id, + id: this.collection.id, + options: { + fields: [], + page: 1, + perPage: collectionCommentStats.total, + }, }); - return CollectWithSubCaseResponseRt.encode({ - subCase: flattenSubCaseSavedObject({ - savedObject: this.subCase, - comments: subCaseComments.saved_objects, - totalAlerts: countAlertsForID({ comments: subCaseComments, id: this.subCase.id }), - }), + const collectionTotalAlerts = + countAlertsForID({ comments: collectionComments, id: this.collection.id }) ?? 0; + + const caseResponse = { + comments: flattenCommentSavedObjects(collectionComments.saved_objects), + totalAlerts: collectionTotalAlerts, ...this.formatCollectionForEncoding(collectionCommentStats.total), - }); - } + }; - const collectionComments = await this.service.getAllCaseComments({ - client: this.soClient, - id: this.collection.id, - options: { - fields: [], - page: 1, - perPage: collectionCommentStats.total, - }, - }); + if (this.subCase) { + const subCaseComments = await this.service.getAllSubCaseComments({ + client: this.soClient, + id: this.subCase.id, + }); + const totalAlerts = + countAlertsForID({ comments: subCaseComments, id: this.subCase.id }) ?? 0; + + return CaseResponseRt.encode({ + ...caseResponse, + /** + * For now we need the sub case comments and totals to be exposed on the top level of the response so that the UI + * functionality can stay the same. Ideally in the future we can refactor this so that the UI will look for the + * comments either in the top level for a case or a collection or in the subCases field if it is a sub case. + * + * If we ever need to return both the collection's comments and the sub case comments we'll need to refactor it then + * as well. + */ + comments: flattenCommentSavedObjects(subCaseComments.saved_objects), + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + subCases: [ + flattenSubCaseSavedObject({ + savedObject: this.subCase, + totalComment: subCaseComments.saved_objects.length, + totalAlerts, + }), + ], + }); + } - return CollectWithSubCaseResponseRt.encode({ - comments: flattenCommentSavedObjects(collectionComments.saved_objects), - totalAlerts: countAlertsForID({ comments: collectionComments, id: this.collection.id }), - ...this.formatCollectionForEncoding(collectionCommentStats.total), - }); + return CaseResponseRt.encode(caseResponse); + } catch (error) { + throw createCaseError({ + message: `Failed encoding the commentable case, sub case id: ${this.subCaseId} case id: ${this.caseId}: ${error}`, + error, + logger: this.logger, + }); + } } } diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 4be519858db18..e4c29bb099f0e 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -18,7 +18,6 @@ import { AssociationType, CaseResponse, CasesResponse, - CollectionWithSubCaseResponse, } from '../../../common/api'; import { connectorMappingsServiceMock, @@ -1018,9 +1017,10 @@ describe('case connector', () => { describe('addComment', () => { it('executes correctly', async () => { - const commentReturn: CollectionWithSubCaseResponse = { + const commentReturn: CaseResponse = { id: 'mock-it', totalComment: 0, + totalAlerts: 0, version: 'WzksMV0=', closed_at: null, diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index fd2fc7389f9ca..4a1d0569bde6d 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -6,7 +6,7 @@ */ import { curry } from 'lodash'; - +import { Logger } from 'src/core/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; import { CasePatchRequest, @@ -26,6 +26,7 @@ import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; import { nullUser } from '../../common'; +import { createCaseError } from '../../common/error'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; @@ -84,6 +85,7 @@ async function executor( connectorMappingsService, userActionService, alertsService, + logger, }); if (!supportedSubActions.includes(subAction)) { @@ -93,9 +95,17 @@ async function executor( } if (subAction === 'create') { - data = await caseClient.create({ - ...(subActionParams as CasePostRequest), - }); + try { + data = await caseClient.create({ + ...(subActionParams as CasePostRequest), + }); + } catch (error) { + throw createCaseError({ + message: `Failed to create a case using connector: ${error}`, + error, + logger, + }); + } } if (subAction === 'update') { @@ -107,13 +117,29 @@ async function executor( {} as CasePatchRequest ); - data = await caseClient.update({ cases: [updateParamsWithoutNullValues] }); + try { + data = await caseClient.update({ cases: [updateParamsWithoutNullValues] }); + } catch (error) { + throw createCaseError({ + message: `Failed to update case using connector id: ${updateParamsWithoutNullValues?.id} version: ${updateParamsWithoutNullValues?.version}: ${error}`, + error, + logger, + }); + } } if (subAction === 'addComment') { const { caseId, comment } = subActionParams as ExecutorSubActionAddCommentParams; - const formattedComment = transformConnectorComment(comment); - data = await caseClient.addComment({ caseId, comment: formattedComment }); + try { + const formattedComment = transformConnectorComment(comment, logger); + data = await caseClient.addComment({ caseId, comment: formattedComment }); + } catch (error) { + throw createCaseError({ + message: `Failed to create comment using connector case id: ${caseId}: ${error}`, + error, + logger, + }); + } } return { status: 'ok', data: data ?? {}, actionId }; @@ -127,7 +153,19 @@ interface AttachmentAlerts { indices: string[]; rule: { id: string | null; name: string | null }; } -export const transformConnectorComment = (comment: CommentSchemaType): CommentRequest => { + +/** + * Convert a connector style comment passed through the action plugin to the expected format for the add comment functionality. + * + * @param comment an object defining the comment to be attached to a case/sub case + * @param logger an optional logger to handle logging an error if parsing failed + * + * Note: This is exported so that the integration tests can use it. + */ +export const transformConnectorComment = ( + comment: CommentSchemaType, + logger?: Logger +): CommentRequest => { if (isCommentGeneratedAlert(comment)) { try { const genAlerts: Array<{ @@ -162,7 +200,11 @@ export const transformConnectorComment = (comment: CommentSchemaType): CommentRe rule, }; } catch (e) { - throw new Error(`Error parsing generated alert in case connector -> ${e.message}`); + throw createCaseError({ + message: `Error parsing generated alert in case connector -> ${e}`, + error: e, + logger, + }); } } else { return comment; diff --git a/x-pack/plugins/case/server/connectors/case/types.ts b/x-pack/plugins/case/server/connectors/case/types.ts index 50ff104d7bad0..6a7dfd9c2e687 100644 --- a/x-pack/plugins/case/server/connectors/case/types.ts +++ b/x-pack/plugins/case/server/connectors/case/types.ts @@ -16,7 +16,7 @@ import { ConnectorSchema, CommentSchema, } from './schema'; -import { CaseResponse, CasesResponse, CollectionWithSubCaseResponse } from '../../../common/api'; +import { CaseResponse, CasesResponse } from '../../../common/api'; export type CaseConfiguration = TypeOf; export type Connector = TypeOf; @@ -29,7 +29,7 @@ export type ExecutorSubActionAddCommentParams = TypeOf< >; export type CaseExecutorParams = TypeOf; -export type CaseExecutorResponse = CaseResponse | CasesResponse | CollectionWithSubCaseResponse; +export type CaseExecutorResponse = CaseResponse | CasesResponse; export type CaseActionType = ActionType< CaseConfiguration, diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 1c00c26a7c0b0..43daa51958429 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -97,11 +97,13 @@ export class CasePlugin { connectorMappingsService: this.connectorMappingsService, userActionService: this.userActionService, alertsService: this.alertsService, + logger: this.log, }) ); const router = core.http.createRouter(); initCaseApi({ + logger: this.log, caseService: this.caseService, caseConfigureService: this.caseConfigureService, connectorMappingsService: this.connectorMappingsService, @@ -137,6 +139,7 @@ export class CasePlugin { connectorMappingsService: this.connectorMappingsService!, userActionService: this.userActionService!, alertsService: this.alertsService!, + logger: this.log, }); }; @@ -156,6 +159,7 @@ export class CasePlugin { connectorMappingsService, userActionService, alertsService, + logger, }: { core: CoreSetup; caseService: CaseServiceSetup; @@ -163,6 +167,7 @@ export class CasePlugin { connectorMappingsService: ConnectorMappingsServiceSetup; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; + logger: Logger; }): IContextProvider => { return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); @@ -178,6 +183,7 @@ export class CasePlugin { userActionService, alertsService, user, + logger, }); }, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts index b4230a05749a1..6b0c4adf9a680 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -35,6 +35,7 @@ export const createRoute = async ( postUserActions: jest.fn(), getUserActions: jest.fn(), }, + logger: log, }); return router[method].mock.calls[0][1]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 492be96fb4aa9..7f66602c61fff 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -57,6 +57,7 @@ export const createRouteContext = async (client: any, badAuth = false) => { userActionService, alertsService, scopedClusterClient: esClient, + logger: log, }); return { context, services: { userActionService } }; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index bcbf1828e1fde..fd250b74fff1e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -13,7 +13,12 @@ import { wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { AssociationType } from '../../../../../common/api'; -export function initDeleteAllCommentsApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteAllCommentsApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASE_COMMENTS_URL, @@ -23,7 +28,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), }, @@ -35,11 +40,11 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic const { username, full_name, email } = await caseService.getUser({ request }); const deleteDate = new Date().toISOString(); - const id = request.query?.subCaseID ?? request.params.case_id; + const id = request.query?.subCaseId ?? request.params.case_id; const comments = await caseService.getCommentsByAssociation({ client, id, - associationType: request.query?.subCaseID + associationType: request.query?.subCaseId ? AssociationType.subCase : AssociationType.case, }); @@ -61,7 +66,7 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: request.params.case_id, - subCaseId: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, commentId: comment.id, fields: ['comment'], }) @@ -70,6 +75,9 @@ export function initDeleteAllCommentsApi({ caseService, router, userActionServic return response.noContent(); } catch (error) { + logger.error( + `Failed to delete all comments in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index 73307753a550d..f1c5fdc2b7cc8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -14,7 +14,12 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initDeleteCommentApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteCommentApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: CASE_COMMENT_DETAILS_URL, @@ -25,7 +30,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), }, @@ -46,8 +51,8 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: throw Boom.notFound(`This comment ${request.params.comment_id} does not exist anymore.`); } - const type = request.query?.subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; - const id = request.query?.subCaseID ?? request.params.case_id; + const type = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const id = request.query?.subCaseId ?? request.params.case_id; const caseRef = myComment.references.find((c) => c.type === type); if (caseRef == null || (caseRef != null && caseRef.id !== id)) { @@ -69,7 +74,7 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: actionAt: deleteDate, actionBy: { username, full_name, email }, caseId: id, - subCaseId: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, commentId: request.params.comment_id, fields: ['comment'], }), @@ -78,6 +83,9 @@ export function initDeleteCommentApi({ caseService, router, userActionService }: return response.noContent(); } catch (error) { + logger.error( + `Failed to delete comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 3431c340c791e..57ddd84e8742c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -27,10 +27,10 @@ import { defaultPage, defaultPerPage } from '../..'; const FindQueryParamsRt = rt.partial({ ...SavedObjectFindOptionsRt.props, - subCaseID: rt.string, + subCaseId: rt.string, }); -export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { +export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${CASE_COMMENTS_URL}/_find`, @@ -49,8 +49,8 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { fold(throwErrors(Boom.badRequest), identity) ); - const id = query.subCaseID ?? request.params.case_id; - const associationType = query.subCaseID ? AssociationType.subCase : AssociationType.case; + const id = query.subCaseId ?? request.params.case_id; + const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; const args = query ? { caseService, @@ -82,6 +82,9 @@ export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { const theComments = await caseService.getCommentsByAssociation(args); return response.ok({ body: CommentsResponseRt.encode(transformComments(theComments)) }); } catch (error) { + logger.error( + `Failed to find comments in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 730b1b92a8a07..770efe0109744 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -14,7 +14,7 @@ import { flattenCommentSavedObjects, wrapError } from '../../utils'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { defaultSortField } from '../../../../common'; -export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { +export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENTS_URL, @@ -25,7 +25,7 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { query: schema.maybe( schema.object({ includeSubCaseComments: schema.maybe(schema.boolean()), - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), }, @@ -35,10 +35,10 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { const client = context.core.savedObjects.client; let comments: SavedObjectsFindResponse; - if (request.query?.subCaseID) { + if (request.query?.subCaseId) { comments = await caseService.getAllSubCaseComments({ client, - id: request.query.subCaseID, + id: request.query.subCaseId, options: { sortField: defaultSortField, }, @@ -58,6 +58,9 @@ export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { body: AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects)), }); } catch (error) { + logger.error( + `Failed to get all comments in route case id: ${request.params.case_id} include sub case comments: ${request.query?.includeSubCaseComments} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts index 6484472af0c3c..9dedfccd3a250 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; -export function initGetCommentApi({ caseService, router }: RouteDeps) { +export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_COMMENT_DETAILS_URL, @@ -35,6 +35,9 @@ export function initGetCommentApi({ caseService, router }: RouteDeps) { body: CommentResponseRt.encode(flattenCommentSavedObject(comment)), }); } catch (error) { + logger.error( + `Failed to get comment in route case id: ${request.params.case_id} comment id: ${request.params.comment_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index e8b6f7bc957eb..f5db2dc004a1d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -12,7 +12,7 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { CommentableCase } from '../../../../common'; import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; @@ -26,11 +26,18 @@ interface CombinedCaseParams { service: CaseServiceSetup; client: SavedObjectsClientContract; caseID: string; - subCaseID?: string; + logger: Logger; + subCaseId?: string; } -async function getCommentableCase({ service, client, caseID, subCaseID }: CombinedCaseParams) { - if (subCaseID) { +async function getCommentableCase({ + service, + client, + caseID, + subCaseId, + logger, +}: CombinedCaseParams) { + if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ service.getCase({ client, @@ -38,25 +45,26 @@ async function getCommentableCase({ service, client, caseID, subCaseID }: Combin }), service.getSubCase({ client, - id: subCaseID, + id: subCaseId, }), ]); - return new CommentableCase({ collection: caseInfo, service, subCase, soClient: client }); + return new CommentableCase({ + collection: caseInfo, + service, + subCase, + soClient: client, + logger, + }); } else { const caseInfo = await service.getCase({ client, id: caseID, }); - return new CommentableCase({ collection: caseInfo, service, soClient: client }); + return new CommentableCase({ collection: caseInfo, service, soClient: client, logger }); } } -export function initPatchCommentApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPatchCommentApi({ caseService, router, userActionService, logger }: RouteDeps) { router.patch( { path: CASE_COMMENTS_URL, @@ -66,7 +74,7 @@ export function initPatchCommentApi({ }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), body: escapeHatch, @@ -87,7 +95,8 @@ export function initPatchCommentApi({ service: caseService, client, caseID: request.params.case_id, - subCaseID: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, + logger, }); const myComment = await caseService.getComment({ @@ -103,7 +112,7 @@ export function initPatchCommentApi({ throw Boom.badRequest(`You cannot change the type of the comment.`); } - const saveObjType = request.query?.subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; + const saveObjType = request.query?.subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const caseRef = myComment.references.find((c) => c.type === saveObjType); if (caseRef == null || (caseRef != null && caseRef.id !== commentableCase.id)) { @@ -144,7 +153,7 @@ export function initPatchCommentApi({ actionAt: updatedDate, actionBy: { username, full_name, email }, caseId: request.params.case_id, - subCaseId: request.query?.subCaseID, + subCaseId: request.query?.subCaseId, commentId: updatedComment.id, fields: ['comment'], newValue: JSON.stringify(queryRestAttributes), @@ -161,6 +170,9 @@ export function initPatchCommentApi({ body: await updatedCase.encode(), }); } catch (error) { + logger.error( + `Failed to patch comment in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 95b611950bd41..b8dc43dbf3fae 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../../types'; import { CASE_COMMENTS_URL } from '../../../../../common/constants'; import { CommentRequest } from '../../../../../common/api'; -export function initPostCommentApi({ router }: RouteDeps) { +export function initPostCommentApi({ router, logger }: RouteDeps) { router.post( { path: CASE_COMMENTS_URL, @@ -21,7 +21,7 @@ export function initPostCommentApi({ router }: RouteDeps) { }), query: schema.maybe( schema.object({ - subCaseID: schema.maybe(schema.string()), + subCaseId: schema.maybe(schema.string()), }) ), body: escapeHatch, @@ -33,7 +33,7 @@ export function initPostCommentApi({ router }: RouteDeps) { } const caseClient = context.case.getCaseClient(); - const caseId = request.query?.subCaseID ?? request.params.case_id; + const caseId = request.query?.subCaseId ?? request.params.case_id; const comment = request.body as CommentRequest; try { @@ -41,6 +41,9 @@ export function initPostCommentApi({ router }: RouteDeps) { body: await caseClient.addComment({ caseId, comment }), }); } catch (error) { + logger.error( + `Failed to post comment in route case id: ${request.params.case_id} sub case id: ${request.query?.subCaseId}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 33226d39a2595..2ca34d25482dd 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -12,7 +12,7 @@ import { wrapError } from '../../utils'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { transformESConnectorToCaseConnector } from '../helpers'; -export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps) { +export function initGetCaseConfigure({ caseConfigureService, router, logger }: RouteDeps) { router.get( { path: CASE_CONFIGURE_URL, @@ -63,6 +63,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps : {}, }); } catch (error) { + logger.error(`Failed to get case configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 0a368e0276bb5..81ffc06355ff5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -28,7 +28,7 @@ const isConnectorSupported = ( * Be aware that this api will only return 20 connectors */ -export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { +export function initCaseConfigureGetActionConnector({ router, logger }: RouteDeps) { router.get( { path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, @@ -52,6 +52,7 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { ); return response.ok({ body: results }); } catch (error) { + logger.error(`Failed to get connectors in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 02d39465373f9..cd764bb0e8a3e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -24,7 +24,12 @@ import { transformESConnectorToCaseConnector, } from '../helpers'; -export function initPatchCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { +export function initPatchCaseConfigure({ + caseConfigureService, + caseService, + router, + logger, +}: RouteDeps) { router.patch( { path: CASE_CONFIGURE_URL, @@ -107,6 +112,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout }), }); } catch (error) { + logger.error(`Failed to get patch configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index db3d5cd6a2e56..f619a727e2e7a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -24,7 +24,12 @@ import { transformESConnectorToCaseConnector, } from '../helpers'; -export function initPostCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { +export function initPostCaseConfigure({ + caseConfigureService, + caseService, + router, + logger, +}: RouteDeps) { router.post( { path: CASE_CONFIGURE_URL, @@ -96,6 +101,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route }), }); } catch (error) { + logger.error(`Failed to post case configure in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 497e33d7feb30..5f2a6c67220c3 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -46,7 +46,7 @@ async function deleteSubCases({ ); } -export function initDeleteCasesApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteCasesApi({ caseService, router, userActionService, logger }: RouteDeps) { router.delete( { path: CASES_URL, @@ -111,6 +111,9 @@ export function initDeleteCasesApi({ caseService, router, userActionService }: R return response.noContent(); } catch (error) { + logger.error( + `Failed to delete cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index 8ba83b42c06d7..d04f01eb73537 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -22,7 +22,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { constructQueryOptions } from './helpers'; -export function initFindCasesApi({ caseService, caseConfigureService, router }: RouteDeps) { +export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${CASES_URL}/_find`, @@ -77,6 +77,7 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }: ), }); } catch (error) { + logger.error(`Failed to find cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index a3311796fa5cd..8a34e3a5b2431 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../types'; import { wrapError } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; -export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) { +export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( { path: CASE_DETAILS_URL, @@ -26,10 +26,10 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }, }, async (context, request, response) => { - const caseClient = context.case.getCaseClient(); - const id = request.params.case_id; - try { + const caseClient = context.case.getCaseClient(); + const id = request.params.case_id; + return response.ok({ body: await caseClient.get({ id, @@ -38,6 +38,9 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }), }); } catch (error) { + logger.error( + `Failed to retrieve case in route case id: ${request.params.case_id} \ninclude comments: ${request.query.includeComments} \ninclude sub comments: ${request.query.includeSubCaseComments}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 67d4d21a57634..2bff6000d5d6a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { CasesPatchRequest } from '../../../../common/api'; -export function initPatchCasesApi({ router }: RouteDeps) { +export function initPatchCasesApi({ router, logger }: RouteDeps) { router.patch( { path: CASES_URL, @@ -19,18 +19,19 @@ export function initPatchCasesApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const cases = request.body as CasesPatchRequest; + const caseClient = context.case.getCaseClient(); + const cases = request.body as CasesPatchRequest; - try { return response.ok({ body: await caseClient.update(cases), }); } catch (error) { + logger.error(`Failed to patch cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 349ed6c3e5af9..1328d95826130 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../types'; import { CASES_URL } from '../../../../common/constants'; import { CasePostRequest } from '../../../../common/api'; -export function initPostCaseApi({ router }: RouteDeps) { +export function initPostCaseApi({ router, logger }: RouteDeps) { router.post( { path: CASES_URL, @@ -20,17 +20,18 @@ export function initPostCaseApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } - const caseClient = context.case.getCaseClient(); - const theCase = request.body as CasePostRequest; - try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + const caseClient = context.case.getCaseClient(); + const theCase = request.body as CasePostRequest; + return response.ok({ body: await caseClient.create({ ...theCase }), }); } catch (error) { + logger.error(`Failed to post case in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index c1f0a2cb59cb1..cfd2f6b9a61ad 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -16,7 +16,7 @@ import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; import { RouteDeps } from '../types'; import { CASE_PUSH_URL } from '../../../../common/constants'; -export function initPushCaseApi({ router }: RouteDeps) { +export function initPushCaseApi({ router, logger }: RouteDeps) { router.post( { path: CASE_PUSH_URL, @@ -26,18 +26,18 @@ export function initPushCaseApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const actionsClient = context.actions?.getActionsClient(); + const caseClient = context.case.getCaseClient(); + const actionsClient = context.actions?.getActionsClient(); - if (actionsClient == null) { - return response.badRequest({ body: 'Action client not found' }); - } + if (actionsClient == null) { + return response.badRequest({ body: 'Action client not found' }); + } - try { const params = pipe( CasePushRequestParamsRt.decode(request.params), fold(throwErrors(Boom.badRequest), identity) @@ -51,6 +51,7 @@ export function initPushCaseApi({ router }: RouteDeps) { }), }); } catch (error) { + logger.error(`Failed to push case in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts index 3d724ca5fc966..e5433f4972239 100644 --- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_REPORTERS_URL } from '../../../../../common/constants'; -export function initGetReportersApi({ caseService, router }: RouteDeps) { +export function initGetReportersApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_REPORTERS_URL, @@ -24,6 +24,7 @@ export function initGetReportersApi({ caseService, router }: RouteDeps) { }); return response.ok({ body: UsersRt.encode(reporters) }); } catch (error) { + logger.error(`Failed to get reporters in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts index f3cd0e2bdda5c..d0addfff09124 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts @@ -12,7 +12,7 @@ import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api'; import { CASE_STATUS_URL } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; -export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { +export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps) { router.get( { path: CASE_STATUS_URL, @@ -41,6 +41,7 @@ export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { }), }); } catch (error) { + logger.error(`Failed to get status stats in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts index db701dd0fc82b..fd33afbd7df8e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -13,7 +13,12 @@ import { wrapError } from '../../utils'; import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; -export function initDeleteSubCasesApi({ caseService, router, userActionService }: RouteDeps) { +export function initDeleteSubCasesApi({ + caseService, + router, + userActionService, + logger, +}: RouteDeps) { router.delete( { path: SUB_CASES_PATCH_DEL_URL, @@ -80,6 +85,9 @@ export function initDeleteSubCasesApi({ caseService, router, userActionService } return response.noContent(); } catch (error) { + logger.error( + `Failed to delete sub cases in route ids: ${JSON.stringify(request.query.ids)}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts index 98052ccaeaba8..c24dde1944f83 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -24,7 +24,7 @@ import { SUB_CASES_URL } from '../../../../../common/constants'; import { constructQueryOptions } from '../helpers'; import { defaultPage, defaultPerPage } from '../..'; -export function initFindSubCasesApi({ caseService, router }: RouteDeps) { +export function initFindSubCasesApi({ caseService, router, logger }: RouteDeps) { router.get( { path: `${SUB_CASES_URL}/_find`, @@ -88,6 +88,9 @@ export function initFindSubCasesApi({ caseService, router }: RouteDeps) { ), }); } catch (error) { + logger.error( + `Failed to find sub cases in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts index b6d9a7345dbdd..32dcc924e1a08 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/get_sub_case.ts @@ -13,7 +13,7 @@ import { flattenSubCaseSavedObject, wrapError } from '../../utils'; import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; import { countAlertsForID } from '../../../../common'; -export function initGetSubCaseApi({ caseService, router }: RouteDeps) { +export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { router.get( { path: SUB_CASE_DETAILS_URL, @@ -70,6 +70,9 @@ export function initGetSubCaseApi({ caseService, router }: RouteDeps) { ), }); } catch (error) { + logger.error( + `Failed to get sub case in route case id: ${request.params.case_id} sub case id: ${request.params.sub_case_id} include comments: ${request.query?.includeComments}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index ca5cd657a39f3..73aacc2c2b0ba 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -14,6 +14,7 @@ import { KibanaRequest, SavedObject, SavedObjectsFindResponse, + Logger, } from 'kibana/server'; import { CaseClient } from '../../../../client'; @@ -47,6 +48,7 @@ import { import { getCaseToUpdate } from '../helpers'; import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; import { addAlertInfoToStatusMap } from '../../../../common'; +import { createCaseError } from '../../../../common/error'; interface UpdateArgs { client: SavedObjectsClientContract; @@ -55,6 +57,7 @@ interface UpdateArgs { request: KibanaRequest; caseClient: CaseClient; subCases: SubCasesPatchRequest; + logger: Logger; } function checkNonExistingOrConflict( @@ -153,8 +156,8 @@ async function getParentCases({ return parentCases.saved_objects.reduce((acc, so) => { const subCaseIDsWithParent = parentIDInfo.parentIDToSubID.get(so.id); - subCaseIDsWithParent?.forEach((subCaseID) => { - acc.set(subCaseID, so); + subCaseIDsWithParent?.forEach((subCaseId) => { + acc.set(subCaseId, so); }); return acc; }, new Map>()); @@ -216,41 +219,53 @@ async function updateAlerts({ caseService, client, caseClient, + logger, }: { subCasesToSync: SubCasePatchRequest[]; caseService: CaseServiceSetup; client: SavedObjectsClientContract; caseClient: CaseClient; + logger: Logger; }) { - const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { - acc.set(subCase.id, subCase); - return acc; - }, new Map()); - // get all the alerts for all sub cases that need to be synced - const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); - // create a map of the status (open, closed, etc) to alert info that needs to be updated - const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const id = getID(alertComment); - const status = - id !== undefined - ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open - : CaseStatuses.open; - - addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); - } - return acc; - }, new Map()); - - // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress - for (const [status, alertInfo] of alertsToUpdate.entries()) { - if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { - caseClient.updateAlertsStatus({ - ids: alertInfo.ids, - status, - indices: alertInfo.indices, - }); + try { + const subCasesToSyncMap = subCasesToSync.reduce((acc, subCase) => { + acc.set(subCase.id, subCase); + return acc; + }, new Map()); + // get all the alerts for all sub cases that need to be synced + const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); + // create a map of the status (open, closed, etc) to alert info that needs to be updated + const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const id = getID(alertComment); + const status = + id !== undefined + ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open + : CaseStatuses.open; + + addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); + } + return acc; + }, new Map()); + + // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress + for (const [status, alertInfo] of alertsToUpdate.entries()) { + if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { + caseClient.updateAlertsStatus({ + ids: alertInfo.ids, + status, + indices: alertInfo.indices, + }); + } } + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status while updating sub cases: ${JSON.stringify( + subCasesToSync + )}: ${error}`, + logger, + error, + }); } } @@ -261,133 +276,152 @@ async function update({ request, caseClient, subCases, + logger, }: UpdateArgs): Promise { const query = pipe( excess(SubCasesPatchRequestRt).decode(subCases), fold(throwErrors(Boom.badRequest), identity) ); - const bulkSubCases = await caseService.getSubCases({ - client, - ids: query.subCases.map((q) => q.id), - }); + try { + const bulkSubCases = await caseService.getSubCases({ + client, + ids: query.subCases.map((q) => q.id), + }); - const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { - acc.set(so.id, so); - return acc; - }, new Map>()); + const subCasesMap = bulkSubCases.saved_objects.reduce((acc, so) => { + acc.set(so.id, so); + return acc; + }, new Map>()); - checkNonExistingOrConflict(query.subCases, subCasesMap); + checkNonExistingOrConflict(query.subCases, subCasesMap); - const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); + const nonEmptySubCaseRequests = getValidUpdateRequests(query.subCases, subCasesMap); - if (nonEmptySubCaseRequests.length <= 0) { - throw Boom.notAcceptable('All update fields are identical to current version.'); - } + if (nonEmptySubCaseRequests.length <= 0) { + throw Boom.notAcceptable('All update fields are identical to current version.'); + } - const subIDToParentCase = await getParentCases({ - client, - caseService, - subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), - subCasesMap, - }); + const subIDToParentCase = await getParentCases({ + client, + caseService, + subCaseIDs: nonEmptySubCaseRequests.map((subCase) => subCase.id), + subCasesMap, + }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request }); - const updatedAt = new Date().toISOString(); - const updatedCases = await caseService.patchSubCases({ - client, - subCases: nonEmptySubCaseRequests.map((thisCase) => { - const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; - let closedInfo: { closed_at: string | null; closed_by: User | null } = { - closed_at: null, - closed_by: null, - }; - - if ( - updateSubCaseAttributes.status && - updateSubCaseAttributes.status === CaseStatuses.closed - ) { - closedInfo = { - closed_at: updatedAt, - closed_by: { email, full_name, username }, - }; - } else if ( - updateSubCaseAttributes.status && - (updateSubCaseAttributes.status === CaseStatuses.open || - updateSubCaseAttributes.status === CaseStatuses['in-progress']) - ) { - closedInfo = { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = await caseService.getUser({ request }); + const updatedAt = new Date().toISOString(); + const updatedCases = await caseService.patchSubCases({ + client, + subCases: nonEmptySubCaseRequests.map((thisCase) => { + const { id: subCaseId, version, ...updateSubCaseAttributes } = thisCase; + let closedInfo: { closed_at: string | null; closed_by: User | null } = { closed_at: null, closed_by: null, }; - } - return { - subCaseId, - updatedAttributes: { - ...updateSubCaseAttributes, - ...closedInfo, - updated_at: updatedAt, - updated_by: { email, full_name, username }, - }, - version, - }; - }), - }); - const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { - const storedSubCase = subCasesMap.get(subCaseToUpdate.id); - const parentCase = subIDToParentCase.get(subCaseToUpdate.id); - return ( - storedSubCase !== undefined && - subCaseToUpdate.status !== undefined && - storedSubCase.attributes.status !== subCaseToUpdate.status && - parentCase?.attributes.settings.syncAlerts - ); - }); + if ( + updateSubCaseAttributes.status && + updateSubCaseAttributes.status === CaseStatuses.closed + ) { + closedInfo = { + closed_at: updatedAt, + closed_by: { email, full_name, username }, + }; + } else if ( + updateSubCaseAttributes.status && + (updateSubCaseAttributes.status === CaseStatuses.open || + updateSubCaseAttributes.status === CaseStatuses['in-progress']) + ) { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } + return { + subCaseId, + updatedAttributes: { + ...updateSubCaseAttributes, + ...closedInfo, + updated_at: updatedAt, + updated_by: { email, full_name, username }, + }, + version, + }; + }), + }); - await updateAlerts({ - caseService, - client, - caseClient, - subCasesToSync: subCasesToSyncAlertsFor, - }); + const subCasesToSyncAlertsFor = nonEmptySubCaseRequests.filter((subCaseToUpdate) => { + const storedSubCase = subCasesMap.get(subCaseToUpdate.id); + const parentCase = subIDToParentCase.get(subCaseToUpdate.id); + return ( + storedSubCase !== undefined && + subCaseToUpdate.status !== undefined && + storedSubCase.attributes.status !== subCaseToUpdate.status && + parentCase?.attributes.settings.syncAlerts + ); + }); - const returnUpdatedSubCases = updatedCases.saved_objects.reduce( - (acc, updatedSO) => { - const originalSubCase = subCasesMap.get(updatedSO.id); - if (originalSubCase) { - acc.push( - flattenSubCaseSavedObject({ - savedObject: { - ...originalSubCase, - ...updatedSO, - attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, - references: originalSubCase.references, - version: updatedSO.version ?? originalSubCase.version, - }, - }) - ); - } - return acc; - }, - [] - ); + await updateAlerts({ + caseService, + client, + caseClient, + subCasesToSync: subCasesToSyncAlertsFor, + logger, + }); - await userActionService.postUserActions({ - client, - actions: buildSubCaseUserActions({ - originalSubCases: bulkSubCases.saved_objects, - updatedSubCases: updatedCases.saved_objects, - actionDate: updatedAt, - actionBy: { email, full_name, username }, - }), - }); + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( + (acc, updatedSO) => { + const originalSubCase = subCasesMap.get(updatedSO.id); + if (originalSubCase) { + acc.push( + flattenSubCaseSavedObject({ + savedObject: { + ...originalSubCase, + ...updatedSO, + attributes: { ...originalSubCase.attributes, ...updatedSO.attributes }, + references: originalSubCase.references, + version: updatedSO.version ?? originalSubCase.version, + }, + }) + ); + } + return acc; + }, + [] + ); + + await userActionService.postUserActions({ + client, + actions: buildSubCaseUserActions({ + originalSubCases: bulkSubCases.saved_objects, + updatedSubCases: updatedCases.saved_objects, + actionDate: updatedAt, + actionBy: { email, full_name, username }, + }), + }); - return SubCasesResponseRt.encode(returnUpdatedSubCases); + return SubCasesResponseRt.encode(returnUpdatedSubCases); + } catch (error) { + const idVersions = query.subCases.map((subCase) => ({ + id: subCase.id, + version: subCase.version, + })); + throw createCaseError({ + message: `Failed to update sub cases: ${JSON.stringify(idVersions)}: ${error}`, + error, + logger, + }); + } } -export function initPatchSubCasesApi({ router, caseService, userActionService }: RouteDeps) { +export function initPatchSubCasesApi({ + router, + caseService, + userActionService, + logger, +}: RouteDeps) { router.patch( { path: SUB_CASES_PATCH_DEL_URL, @@ -396,10 +430,10 @@ export function initPatchSubCasesApi({ router, caseService, userActionService }: }, }, async (context, request, response) => { - const caseClient = context.case.getCaseClient(); - const subCases = request.body as SubCasesPatchRequest; - try { + const caseClient = context.case.getCaseClient(); + const subCases = request.body as SubCasesPatchRequest; + return response.ok({ body: await update({ request, @@ -408,9 +442,11 @@ export function initPatchSubCasesApi({ router, caseService, userActionService }: client: context.core.savedObjects.client, caseService, userActionService, + logger, }), }); } catch (error) { + logger.error(`Failed to patch sub cases in route: ${error}`); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 488f32a795811..2efef9ac67f80 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -11,7 +11,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; -export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { +export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -22,25 +22,28 @@ export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const caseId = request.params.case_id; + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; - try { return response.ok({ body: await caseClient.getUserActions({ caseId }), }); } catch (error) { + logger.error( + `Failed to retrieve case user actions in route case id: ${request.params.case_id}: ${error}` + ); return response.customError(wrapError(error)); } } ); } -export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { +export function initGetAllSubCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( { path: SUB_CASE_USER_ACTIONS_URL, @@ -52,19 +55,22 @@ export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { }, }, async (context, request, response) => { - if (!context.case) { - return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); - } + try { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const caseClient = context.case.getCaseClient(); - const caseId = request.params.case_id; - const subCaseId = request.params.sub_case_id; + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + const subCaseId = request.params.sub_case_id; - try { return response.ok({ body: await caseClient.getUserActions({ caseId, subCaseId }), }); } catch (error) { + logger.error( + `Failed to retrieve sub case user actions in route case id: ${request.params.case_id} sub case id: ${request.params.sub_case_id}: ${error}` + ); return response.customError(wrapError(error)); } } diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index 395880e5c1410..6ce40e01c7752 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; + import type { CaseConfigureServiceSetup, CaseServiceSetup, @@ -20,6 +22,7 @@ export interface RouteDeps { connectorMappingsService: ConnectorMappingsServiceSetup; router: CasesRouter; userActionService: CaseUserActionServiceSetup; + logger: Logger; } export enum SortFieldCase { diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 084b1a17a1434..298f8bb877cda 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash'; -import { badRequest, boomify, isBoom } from '@hapi/boom'; +import { badRequest, Boom, boomify, isBoom } from '@hapi/boom'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -45,6 +45,7 @@ import { import { transformESConnectorToCaseConnector } from './cases/helpers'; import { SortFieldCase } from './types'; +import { isCaseError } from '../../common/error'; export const transformNewSubCase = ({ createdAt, @@ -182,9 +183,19 @@ export const transformNewComment = ({ }; }; +/** + * Transforms an error into the correct format for a kibana response. + */ export function wrapError(error: any): CustomHttpResponseOptions { - const options = { statusCode: error.statusCode ?? 500 }; - const boom = isBoom(error) ? error : boomify(error, options); + let boom: Boom; + + if (isCaseError(error)) { + boom = error.boomify(); + } else { + const options = { statusCode: error.statusCode ?? 500 }; + boom = isBoom(error) ? error : boomify(error, options); + } + return { body: boom, headers: boom.output.headers as { [key: string]: string }, diff --git a/x-pack/plugins/case/server/scripts/sub_cases/index.ts b/x-pack/plugins/case/server/scripts/sub_cases/index.ts index 9dd577c40c74e..ba3bcaa65091c 100644 --- a/x-pack/plugins/case/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/case/server/scripts/sub_cases/index.ts @@ -6,13 +6,9 @@ */ /* eslint-disable no-console */ import yargs from 'yargs'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; -import { - CaseResponse, - CaseType, - CollectionWithSubCaseResponse, - ConnectorTypes, -} from '../../../common/api'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; +import { CaseResponse, CaseType, ConnectorTypes } from '../../../common/api'; import { CommentType } from '../../../common/api/cases/comment'; import { CASES_URL } from '../../../common/constants'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; @@ -118,9 +114,7 @@ async function handleGenGroupAlerts(argv: any) { ), }; - const executeResp = await client.request< - ActionTypeExecutorResult - >({ + const executeResp = await client.request>({ path: `/api/actions/action/${createdAction.data.id}/_execute`, method: 'POST', body: { diff --git a/x-pack/plugins/case/server/services/alerts/index.test.ts b/x-pack/plugins/case/server/services/alerts/index.test.ts index 35aa3ff80efc1..3b1020d3ef556 100644 --- a/x-pack/plugins/case/server/services/alerts/index.test.ts +++ b/x-pack/plugins/case/server/services/alerts/index.test.ts @@ -8,10 +8,11 @@ import { KibanaRequest } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; import { AlertService, AlertServiceContract } from '.'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; describe('updateAlertsStatus', () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); + const logger = loggingSystemMock.create().get('case'); describe('happy path', () => { let alertService: AlertServiceContract; @@ -21,6 +22,7 @@ describe('updateAlertsStatus', () => { request: {} as KibanaRequest, status: CaseStatuses.closed, scopedClusterClient: esClient, + logger, }; beforeEach(async () => { @@ -50,6 +52,7 @@ describe('updateAlertsStatus', () => { status: CaseStatuses.closed, indices: new Set(['']), scopedClusterClient: esClient, + logger, }) ).toBeUndefined(); }); diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index a19e533418bc9..45245b86ba2d5 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -9,9 +9,10 @@ import _ from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { ElasticsearchClient } from 'kibana/server'; +import { ElasticsearchClient, Logger } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; export type AlertServiceContract = PublicMethodsOf; @@ -20,12 +21,14 @@ interface UpdateAlertsStatusArgs { status: CaseStatuses; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } interface GetAlertsArgs { ids: string[]; indices: Set; scopedClusterClient: ElasticsearchClient; + logger: Logger; } interface Alert { @@ -57,56 +60,75 @@ export class AlertService { status, indices, scopedClusterClient, + logger, }: UpdateAlertsStatusArgs) { const sanitizedIndices = getValidIndices(indices); if (sanitizedIndices.length <= 0) { - // log that we only had invalid indices + logger.warn(`Empty alert indices when updateAlertsStatus ids: ${JSON.stringify(ids)}`); return; } - const result = await scopedClusterClient.updateByQuery({ - index: sanitizedIndices, - conflicts: 'abort', - body: { - script: { - source: `ctx._source.signal.status = '${status}'`, - lang: 'painless', + try { + const result = await scopedClusterClient.updateByQuery({ + index: sanitizedIndices, + conflicts: 'abort', + body: { + script: { + source: `ctx._source.signal.status = '${status}'`, + lang: 'painless', + }, + query: { ids: { values: ids } }, }, - query: { ids: { values: ids } }, - }, - ignore_unavailable: true, - }); - - return result; + ignore_unavailable: true, + }); + + return result; + } catch (error) { + throw createCaseError({ + message: `Failed to update alert status ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } } public async getAlerts({ scopedClusterClient, ids, indices, + logger, }: GetAlertsArgs): Promise { const index = getValidIndices(indices); if (index.length <= 0) { + logger.warn(`Empty alert indices when retrieving alerts ids: ${JSON.stringify(ids)}`); return; } - const result = await scopedClusterClient.search({ - index, - body: { - query: { - bool: { - filter: { - ids: { - values: ids, + try { + const result = await scopedClusterClient.search({ + index, + body: { + query: { + bool: { + filter: { + ids: { + values: ids, + }, }, }, }, }, - }, - size: MAX_ALERTS_PER_SUB_CASE, - ignore_unavailable: true, - }); - - return result.body; + size: MAX_ALERTS_PER_SUB_CASE, + ignore_unavailable: true, + }); + + return result.body; + } catch (error) { + throw createCaseError({ + message: `Failed to retrieve alerts ids: ${JSON.stringify(ids)}: ${error}`, + error, + logger, + }); + } } } diff --git a/x-pack/plugins/case/server/services/connector_mappings/index.ts b/x-pack/plugins/case/server/services/connector_mappings/index.ts index cc387d8e6fe3e..d4fda10276d2b 100644 --- a/x-pack/plugins/case/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/case/server/services/connector_mappings/index.ts @@ -41,7 +41,7 @@ export class ConnectorMappingsService { this.log.debug(`Attempting to find all connector mappings`); return await client.find({ ...options, type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT }); } catch (error) { - this.log.debug(`Attempting to find all connector mappings`); + this.log.error(`Attempting to find all connector mappings: ${error}`); throw error; } }, @@ -52,7 +52,7 @@ export class ConnectorMappingsService { references, }); } catch (error) { - this.log.debug(`Error on POST a new connector mappings: ${error}`); + this.log.error(`Error on POST a new connector mappings: ${error}`); throw error; } }, diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index a9e5c26960830..f74e91ca10224 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -621,7 +621,7 @@ export class CaseService implements CaseServiceSetup { ], }); } catch (error) { - this.log.debug(`Error on POST a new sub case: ${error}`); + this.log.error(`Error on POST a new sub case for id ${caseId}: ${error}`); throw error; } } @@ -642,7 +642,7 @@ export class CaseService implements CaseServiceSetup { return subCases.saved_objects[0]; } catch (error) { - this.log.debug(`Error finding the most recent sub case for case: ${caseId}`); + this.log.error(`Error finding the most recent sub case for case: ${caseId}: ${error}`); throw error; } } @@ -652,7 +652,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to DELETE sub case ${id}`); return await client.delete(SUB_CASE_SAVED_OBJECT, id); } catch (error) { - this.log.debug(`Error on DELETE sub case ${id}: ${error}`); + this.log.error(`Error on DELETE sub case ${id}: ${error}`); throw error; } } @@ -662,7 +662,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to DELETE case ${caseId}`); return await client.delete(CASE_SAVED_OBJECT, caseId); } catch (error) { - this.log.debug(`Error on DELETE case ${caseId}: ${error}`); + this.log.error(`Error on DELETE case ${caseId}: ${error}`); throw error; } } @@ -671,7 +671,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET comment ${commentId}`); return await client.delete(CASE_COMMENT_SAVED_OBJECT, commentId); } catch (error) { - this.log.debug(`Error on GET comment ${commentId}: ${error}`); + this.log.error(`Error on GET comment ${commentId}: ${error}`); throw error; } } @@ -683,7 +683,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET case ${caseId}`); return await client.get(CASE_SAVED_OBJECT, caseId); } catch (error) { - this.log.debug(`Error on GET case ${caseId}: ${error}`); + this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; } } @@ -692,7 +692,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET sub case ${id}`); return await client.get(SUB_CASE_SAVED_OBJECT, id); } catch (error) { - this.log.debug(`Error on GET sub case ${id}: ${error}`); + this.log.error(`Error on GET sub case ${id}: ${error}`); throw error; } } @@ -705,7 +705,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET sub cases ${ids.join(', ')}`); return await client.bulkGet(ids.map((id) => ({ type: SUB_CASE_SAVED_OBJECT, id }))); } catch (error) { - this.log.debug(`Error on GET cases ${ids.join(', ')}: ${error}`); + this.log.error(`Error on GET cases ${ids.join(', ')}: ${error}`); throw error; } } @@ -720,7 +720,7 @@ export class CaseService implements CaseServiceSetup { caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); } catch (error) { - this.log.debug(`Error on GET cases ${caseIds.join(', ')}: ${error}`); + this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); throw error; } } @@ -732,7 +732,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET comment ${commentId}`); return await client.get(CASE_COMMENT_SAVED_OBJECT, commentId); } catch (error) { - this.log.debug(`Error on GET comment ${commentId}: ${error}`); + this.log.error(`Error on GET comment ${commentId}: ${error}`); throw error; } } @@ -749,7 +749,7 @@ export class CaseService implements CaseServiceSetup { type: CASE_SAVED_OBJECT, }); } catch (error) { - this.log.debug(`Error on find cases: ${error}`); + this.log.error(`Error on find cases: ${error}`); throw error; } } @@ -786,7 +786,7 @@ export class CaseService implements CaseServiceSetup { type: SUB_CASE_SAVED_OBJECT, }); } catch (error) { - this.log.debug(`Error on find sub cases: ${error}`); + this.log.error(`Error on find sub cases: ${error}`); throw error; } } @@ -824,7 +824,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug( + this.log.error( `Error on GET all sub cases for case collection id ${ids.join(', ')}: ${error}` ); throw error; @@ -847,7 +847,7 @@ export class CaseService implements CaseServiceSetup { options, }: FindCommentsArgs): Promise> { try { - this.log.debug(`Attempting to GET all comments for id ${id}`); + this.log.debug(`Attempting to GET all comments for id ${JSON.stringify(id)}`); if (options?.page !== undefined || options?.perPage !== undefined) { return client.find({ type: CASE_COMMENT_SAVED_OBJECT, @@ -874,7 +874,7 @@ export class CaseService implements CaseServiceSetup { ...options, }); } catch (error) { - this.log.debug(`Error on GET all comments for ${id}: ${error}`); + this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -915,7 +915,7 @@ export class CaseService implements CaseServiceSetup { ); } - this.log.debug(`Attempting to GET all comments for case caseID ${id}`); + this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); return this.getAllComments({ client, id, @@ -927,7 +927,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug(`Error on GET all comments for case ${id}: ${error}`); + this.log.error(`Error on GET all comments for case ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -948,7 +948,7 @@ export class CaseService implements CaseServiceSetup { }; } - this.log.debug(`Attempting to GET all comments for sub case caseID ${id}`); + this.log.debug(`Attempting to GET all comments for sub case caseID ${JSON.stringify(id)}`); return this.getAllComments({ client, id, @@ -959,7 +959,7 @@ export class CaseService implements CaseServiceSetup { }, }); } catch (error) { - this.log.debug(`Error on GET all comments for sub case ${id}: ${error}`); + this.log.error(`Error on GET all comments for sub case ${JSON.stringify(id)}: ${error}`); throw error; } } @@ -969,7 +969,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET all reporters`); return await readReporters({ client }); } catch (error) { - this.log.debug(`Error on GET all reporters: ${error}`); + this.log.error(`Error on GET all reporters: ${error}`); throw error; } } @@ -978,7 +978,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to GET all cases`); return await readTags({ client }); } catch (error) { - this.log.debug(`Error on GET cases: ${error}`); + this.log.error(`Error on GET cases: ${error}`); throw error; } } @@ -1003,7 +1003,7 @@ export class CaseService implements CaseServiceSetup { email: null, }; } catch (error) { - this.log.debug(`Error on GET cases: ${error}`); + this.log.error(`Error on GET cases: ${error}`); throw error; } } @@ -1012,7 +1012,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to POST a new case`); return await client.create(CASE_SAVED_OBJECT, { ...attributes }); } catch (error) { - this.log.debug(`Error on POST a new case: ${error}`); + this.log.error(`Error on POST a new case: ${error}`); throw error; } } @@ -1021,7 +1021,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to POST a new comment`); return await client.create(CASE_COMMENT_SAVED_OBJECT, attributes, { references }); } catch (error) { - this.log.debug(`Error on POST a new comment: ${error}`); + this.log.error(`Error on POST a new comment: ${error}`); throw error; } } @@ -1030,7 +1030,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to UPDATE case ${caseId}`); return await client.update(CASE_SAVED_OBJECT, caseId, { ...updatedAttributes }, { version }); } catch (error) { - this.log.debug(`Error on UPDATE case ${caseId}: ${error}`); + this.log.error(`Error on UPDATE case ${caseId}: ${error}`); throw error; } } @@ -1046,7 +1046,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); + this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); throw error; } } @@ -1062,7 +1062,7 @@ export class CaseService implements CaseServiceSetup { { version } ); } catch (error) { - this.log.debug(`Error on UPDATE comment ${commentId}: ${error}`); + this.log.error(`Error on UPDATE comment ${commentId}: ${error}`); throw error; } } @@ -1080,7 +1080,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug( + this.log.error( `Error on UPDATE comments ${comments.map((c) => c.commentId).join(', ')}: ${error}` ); throw error; @@ -1096,7 +1096,7 @@ export class CaseService implements CaseServiceSetup { { version } ); } catch (error) { - this.log.debug(`Error on UPDATE sub case ${subCaseId}: ${error}`); + this.log.error(`Error on UPDATE sub case ${subCaseId}: ${error}`); throw error; } } @@ -1115,7 +1115,7 @@ export class CaseService implements CaseServiceSetup { })) ); } catch (error) { - this.log.debug( + this.log.error( `Error on UPDATE sub case ${subCases.map((c) => c.subCaseId).join(', ')}: ${error}` ); throw error; diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts index d05ada0dba30c..785c81021b584 100644 --- a/x-pack/plugins/case/server/services/user_actions/index.ts +++ b/x-pack/plugins/case/server/services/user_actions/index.ts @@ -66,7 +66,7 @@ export class CaseUserActionService { sortOrder: 'asc', }); } catch (error) { - this.log.debug(`Error on GET case user action: ${error}`); + this.log.error(`Error on GET case user action case id: ${caseId}: ${error}`); throw error; } }, @@ -77,7 +77,7 @@ export class CaseUserActionService { actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); } catch (error) { - this.log.debug(`Error on POST a new case user action: ${error}`); + this.log.error(`Error on POST a new case user action: ${error}`); throw error; } }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss new file mode 100644 index 0000000000000..c242cf29fd37d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.scss @@ -0,0 +1,3 @@ +.curationQueryRow { + margin-bottom: $euiSizeXS; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx new file mode 100644 index 0000000000000..e55b944f7bebc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions, setMockValues } from '../../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CurationQuery } from './curation_query'; + +import { CurationQueries } from './'; + +describe('CurationQueries', () => { + const props = { + queries: ['a', 'b', 'c'], + onSubmit: jest.fn(), + }; + const values = { + queries: ['a', 'b', 'c'], + hasEmptyQueries: false, + hasOnlyOneQuery: false, + }; + const actions = { + addQuery: jest.fn(), + editQuery: jest.fn(), + deleteQuery: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders a CurationQuery row for each query', () => { + const wrapper = shallow(); + + expect(wrapper.find(CurationQuery)).toHaveLength(3); + expect(wrapper.find(CurationQuery).at(0).prop('queryValue')).toEqual('a'); + expect(wrapper.find(CurationQuery).at(1).prop('queryValue')).toEqual('b'); + expect(wrapper.find(CurationQuery).at(2).prop('queryValue')).toEqual('c'); + }); + + it('calls editQuery when the CurationQuery value changes', () => { + const wrapper = shallow(); + wrapper.find(CurationQuery).at(0).simulate('change', 'new query value'); + + expect(actions.editQuery).toHaveBeenCalledWith(0, 'new query value'); + }); + + it('calls deleteQuery when the CurationQuery calls onDelete', () => { + const wrapper = shallow(); + wrapper.find(CurationQuery).at(2).simulate('delete'); + + expect(actions.deleteQuery).toHaveBeenCalledWith(2); + }); + + it('calls addQuery when the Add Query button is clicked', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="addCurationQueryButton"]').simulate('click'); + + expect(actions.addQuery).toHaveBeenCalled(); + }); + + it('disables the add button if any query fields are empty', () => { + setMockValues({ + ...values, + queries: ['a', '', 'c'], + hasEmptyQueries: true, + }); + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="addCurationQueryButton"]'); + + expect(button.prop('isDisabled')).toEqual(true); + }); + + it('calls the passed onSubmit callback when the submit button is clicked', () => { + setMockValues({ ...values, queries: ['some query'] }); + const wrapper = shallow(); + wrapper.find('[data-test-subj="submitCurationQueriesButton"]').simulate('click'); + + expect(props.onSubmit).toHaveBeenCalledWith(['some query']); + }); + + it('disables the submit button if no query fields have been filled', () => { + setMockValues({ + ...values, + queries: [''], + hasOnlyOneQuery: true, + hasEmptyQueries: true, + }); + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="submitCurationQueriesButton"]'); + + expect(button.prop('isDisabled')).toEqual(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx new file mode 100644 index 0000000000000..ad7872b112408 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { EuiButton, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Curation } from '../../types'; + +import { CurationQueriesLogic } from './curation_queries_logic'; +import { CurationQuery } from './curation_query'; +import { filterEmptyQueries } from './utils'; +import './curation_queries.scss'; + +interface Props { + queries: Curation['queries']; + onSubmit(queries: Curation['queries']): void; + submitButtonText?: string; +} + +export const CurationQueries: React.FC = ({ + queries: initialQueries, + onSubmit, + submitButtonText = i18n.translate('xpack.enterpriseSearch.actions.continue', { + defaultMessage: 'Continue', + }), +}) => { + const logic = CurationQueriesLogic({ queries: initialQueries }); + const { queries, hasEmptyQueries, hasOnlyOneQuery } = useValues(logic); + const { addQuery, editQuery, deleteQuery } = useActions(logic); + + return ( + <> + {queries.map((query: string, index) => ( + editQuery(index, newValue)} + onDelete={() => deleteQuery(index)} + disableDelete={hasOnlyOneQuery} + /> + ))} + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.curations.addQueryButtonLabel', { + defaultMessage: 'Add query', + })} + + + onSubmit(filterEmptyQueries(queries))} + data-test-subj="submitCurationQueriesButton" + > + {submitButtonText} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts new file mode 100644 index 0000000000000..157e97433d2b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resetContext } from 'kea'; + +import { CurationQueriesLogic } from './curation_queries_logic'; + +describe('CurationQueriesLogic', () => { + const MOCK_QUERIES = ['a', 'b', 'c']; + + const DEFAULT_PROPS = { queries: MOCK_QUERIES }; + const DEFAULT_VALUES = { + queries: MOCK_QUERIES, + hasEmptyQueries: false, + hasOnlyOneQuery: false, + }; + + const mount = (props = {}) => { + CurationQueriesLogic({ ...DEFAULT_PROPS, ...props }); + CurationQueriesLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + it('has expected default values passed from props', () => { + mount(); + expect(CurationQueriesLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + afterEach(() => { + // Should not mutate the original array + expect(CurationQueriesLogic.values.queries).not.toBe(MOCK_QUERIES); // Would fail if we did not clone a new array + }); + + describe('addQuery', () => { + it('appends an empty string to the queries array', () => { + mount(); + CurationQueriesLogic.actions.addQuery(); + + expect(CurationQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasEmptyQueries: true, + queries: ['a', 'b', 'c', ''], + }); + }); + }); + + describe('deleteQuery', () => { + it('deletes the query string at the specified array index', () => { + mount(); + CurationQueriesLogic.actions.deleteQuery(1); + + expect(CurationQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + queries: ['a', 'c'], + }); + }); + }); + + describe('editQuery', () => { + it('edits the query string at the specified array index', () => { + mount(); + CurationQueriesLogic.actions.editQuery(2, 'z'); + + expect(CurationQueriesLogic.values).toEqual({ + ...DEFAULT_VALUES, + queries: ['a', 'b', 'z'], + }); + }); + }); + }); + + describe('selectors', () => { + describe('hasEmptyQueries', () => { + it('returns true if queries has any empty strings', () => { + mount({ queries: ['', '', ''] }); + + expect(CurationQueriesLogic.values.hasEmptyQueries).toEqual(true); + }); + }); + + describe('hasOnlyOneQuery', () => { + it('returns true if queries only has one item', () => { + mount({ queries: ['test'] }); + + expect(CurationQueriesLogic.values.hasOnlyOneQuery).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts new file mode 100644 index 0000000000000..98109657d61a3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_queries_logic.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +interface CurationQueriesValues { + queries: string[]; + hasEmptyQueries: boolean; + hasOnlyOneQuery: boolean; +} + +interface CurationQueriesActions { + addQuery(): void; + deleteQuery(indexToDelete: number): { indexToDelete: number }; + editQuery(index: number, newQueryValue: string): { index: number; newQueryValue: string }; +} + +export const CurationQueriesLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'curation_queries_logic'], + actions: () => ({ + addQuery: true, + deleteQuery: (indexToDelete) => ({ indexToDelete }), + editQuery: (index, newQueryValue) => ({ index, newQueryValue }), + }), + reducers: ({ props }) => ({ + queries: [ + props.queries, + { + addQuery: (state) => [...state, ''], + deleteQuery: (state, { indexToDelete }) => { + const newState = [...state]; + newState.splice(indexToDelete, 1); + return newState; + }, + editQuery: (state, { index, newQueryValue }) => { + const newState = [...state]; + newState[index] = newQueryValue; + return newState; + }, + }, + ], + }), + selectors: { + hasEmptyQueries: [(selectors) => [selectors.queries], (queries) => queries.indexOf('') >= 0], + hasOnlyOneQuery: [(selectors) => [selectors.queries], (queries) => queries.length <= 1], + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx new file mode 100644 index 0000000000000..64fbec59382a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFieldText } from '@elastic/eui'; + +import { CurationQuery } from './curation_query'; + +describe('CurationQuery', () => { + const props = { + queryValue: 'some query', + onChange: jest.fn(), + onDelete: jest.fn(), + disableDelete: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText)).toHaveLength(1); + expect(wrapper.find(EuiFieldText).prop('value')).toEqual('some query'); + }); + + it('calls onChange when the input value changes', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldText).simulate('change', { target: { value: 'new query value' } }); + + expect(props.onChange).toHaveBeenCalledWith('new query value'); + }); + + it('calls onDelete when the delete button is clicked', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="deleteCurationQueryButton"]').simulate('click'); + + expect(props.onDelete).toHaveBeenCalled(); + }); + + it('disables the delete button if disableDelete is passed', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="deleteCurationQueryButton"]'); + + expect(button.prop('isDisabled')).toEqual(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.tsx new file mode 100644 index 0000000000000..78b32ef12e361 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/curation_query.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFieldText, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + queryValue: string; + onChange(newValue: string): void; + onDelete(): void; + disableDelete: boolean; +} + +export const CurationQuery: React.FC = ({ + queryValue, + onChange, + onDelete, + disableDelete, +}) => ( + + + onChange(e.target.value)} + /> + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts new file mode 100644 index 0000000000000..4f9136d15d6c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CurationQueries } from './curation_queries'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.ts new file mode 100644 index 0000000000000..d84649f090691 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { filterEmptyQueries } from './utils'; + +describe('filterEmptyQueries', () => { + it('filters out all empty strings from a queries array', () => { + const queries = ['', 'a', '', 'b', '', 'c', '']; + expect(filterEmptyQueries(queries)).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts new file mode 100644 index 0000000000000..505e9641d778e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/curation_queries/utils.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const filterEmptyQueries = (queries: string[]) => { + return queries.filter((query) => query.length); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts new file mode 100644 index 0000000000000..4f9136d15d6c3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CurationQueries } from './curation_queries'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts index 1505fe5136bda..c1031fc20bc15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; +import { + LogicMounter, + mockHttpValues, + mockKibanaValues, + mockFlashMessageHelpers, +} from '../../../__mocks__'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; @@ -17,6 +22,7 @@ import { CurationsLogic } from './'; describe('CurationsLogic', () => { const { mount } = new LogicMounter(CurationsLogic); const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; const { clearFlashMessages, setSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; const MOCK_CURATIONS_RESPONSE = { @@ -128,7 +134,7 @@ describe('CurationsLogic', () => { }); }); - describe('deleteCurationSet', () => { + describe('deleteCuration', () => { const confirmSpy = jest.spyOn(window, 'confirm'); beforeEach(() => { @@ -140,7 +146,7 @@ describe('CurationsLogic', () => { mount(); jest.spyOn(CurationsLogic.actions, 'loadCurations'); - CurationsLogic.actions.deleteCurationSet('some-curation-id'); + CurationsLogic.actions.deleteCuration('some-curation-id'); expect(clearFlashMessages).toHaveBeenCalled(); await nextTick(); @@ -155,7 +161,7 @@ describe('CurationsLogic', () => { http.delete.mockReturnValueOnce(Promise.reject('error')); mount(); - CurationsLogic.actions.deleteCurationSet('some-curation-id'); + CurationsLogic.actions.deleteCuration('some-curation-id'); expect(clearFlashMessages).toHaveBeenCalled(); await nextTick(); @@ -166,12 +172,39 @@ describe('CurationsLogic', () => { confirmSpy.mockImplementationOnce(() => false); mount(); - CurationsLogic.actions.deleteCurationSet('some-curation-id'); + CurationsLogic.actions.deleteCuration('some-curation-id'); expect(clearFlashMessages).toHaveBeenCalled(); await nextTick(); expect(http.delete).not.toHaveBeenCalled(); }); }); + + describe('createCuration', () => { + it('should make an API call and navigate to the new curation', async () => { + http.post.mockReturnValueOnce(Promise.resolve({ id: 'some-cur-id' })); + mount(); + + CurationsLogic.actions.createCuration(['some query']); + expect(clearFlashMessages).toHaveBeenCalled(); + await nextTick(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/engines/some-engine/curations', { + body: '{"queries":["some query"]}', + }); + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/some-cur-id'); + }); + + it('handles errors', async () => { + http.post.mockReturnValueOnce(Promise.reject('error')); + mount(); + + CurationsLogic.actions.createCuration(['some query']); + expect(clearFlashMessages).toHaveBeenCalled(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts index 434aff9c3cc4b..f4916f54fbc22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.ts @@ -15,8 +15,10 @@ import { flashAPIErrors, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { updateMetaPageIndex } from '../../../shared/table_pagination'; -import { EngineLogic } from '../engine'; +import { ENGINE_CURATION_PATH } from '../../routes'; +import { EngineLogic, generateEnginePath } from '../engine'; import { DELETE_MESSAGE, SUCCESS_MESSAGE } from './constants'; import { Curation, CurationsAPIResponse } from './types'; @@ -31,7 +33,8 @@ interface CurationsActions { onCurationsLoad(response: CurationsAPIResponse): CurationsAPIResponse; onPaginate(newPageIndex: number): { newPageIndex: number }; loadCurations(): void; - deleteCurationSet(id: string): string; + deleteCuration(id: string): string; + createCuration(queries: Curation['queries']): Curation['queries']; } export const CurationsLogic = kea>({ @@ -40,7 +43,8 @@ export const CurationsLogic = kea ({ results, meta }), onPaginate: (newPageIndex) => ({ newPageIndex }), loadCurations: true, - deleteCurationSet: (id) => id, + deleteCuration: (id) => id, + createCuration: (queries) => queries, }), reducers: () => ({ dataLoading: [ @@ -82,7 +86,7 @@ export const CurationsLogic = kea { + deleteCuration: async (id) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; clearFlashMessages(); @@ -97,5 +101,20 @@ export const CurationsLogic = kea { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { navigateToUrl } = KibanaLogic.values; + clearFlashMessages(); + + try { + const response = await http.post(`/api/app_search/engines/${engineName}/curations`, { + body: JSON.stringify({ queries }), + }); + navigateToUrl(generateEnginePath(ENGINE_CURATION_PATH, { curationId: response.id })); + } catch (e) { + flashAPIErrors(e); + } + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx index b4479fb145f81..634736bca4c65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx @@ -19,8 +19,8 @@ import { ENGINE_CURATION_ADD_RESULT_PATH, } from '../../routes'; -import { CURATIONS_TITLE } from './constants'; -import { Curations } from './views'; +import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants'; +import { Curations, CurationCreation } from './views'; interface Props { engineBreadcrumb: BreadcrumbTrail; @@ -35,8 +35,8 @@ export const CurationsRouter: React.FC = ({ engineBreadcrumb }) => { - - TODO: Curation creation view + + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx new file mode 100644 index 0000000000000..e6ddbb9c1b7a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { CurationQueries } from '../components'; + +import { CurationCreation } from './curation_creation'; + +describe('CurationCreation', () => { + const actions = { + createCuration: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(CurationQueries)).toHaveLength(1); + }); + + it('calls createCuration on CurationQueries submit', () => { + const wrapper = shallow(); + wrapper.find(CurationQueries).simulate('submit', ['some query']); + + expect(actions.createCuration).toHaveBeenCalledWith(['some query']); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx new file mode 100644 index 0000000000000..b1bfc6c2ab7fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../../shared/flash_messages'; + +import { CurationQueries } from '../components'; +import { CREATE_NEW_CURATION_TITLE } from '../constants'; +import { CurationsLogic } from '../index'; + +export const CurationCreation: React.FC = () => { + const { createCuration } = useActions(CurationsLogic); + + return ( + <> + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.create.curationQueriesTitle', + { defaultMessage: 'Curation queries' } + )} +

+
+ +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.curations.create.curationQueriesDescription', + { + defaultMessage: + 'Add one or multiple queries to curate. You will be able add or remove more queries later.', + } + )} +

+
+ + createCuration(queries)} /> +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index fd5d5b7ea64a9..d06144023e170 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -51,7 +51,7 @@ describe('Curations', () => { const actions = { loadCurations: jest.fn(), - deleteCurationSet: jest.fn(), + deleteCuration: jest.fn(), onPaginate: jest.fn(), }; @@ -134,12 +134,12 @@ describe('Curations', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations/cur-id-2'); }); - it('delete action calls deleteCurationSet', () => { + it('delete action calls deleteCuration', () => { wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').first().simulate('click'); - expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-1'); + expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-1'); wrapper.find('[data-test-subj="CurationsTableDeleteButton"]').last().simulate('click'); - expect(actions.deleteCurationSet).toHaveBeenCalledWith('cur-id-2'); + expect(actions.deleteCuration).toHaveBeenCalledWith('cur-id-2'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 6affef53d71ee..fd0a36dfebec7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -69,7 +69,7 @@ export const Curations: React.FC = () => { export const CurationsTable: React.FC = () => { const { dataLoading, curations, meta } = useValues(CurationsLogic); - const { onPaginate, deleteCurationSet } = useActions(CurationsLogic); + const { onPaginate, deleteCuration } = useActions(CurationsLogic); const columns: Array> = [ { @@ -141,7 +141,7 @@ export const CurationsTable: React.FC = () => { type: 'icon', icon: 'trash', color: 'danger', - onClick: (curation: Curation) => deleteCurationSet(curation.id), + onClick: (curation: Curation) => deleteCuration(curation.id), 'data-test-subj': 'CurationsTableDeleteButton', }, ], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts index d454d24f6c8b5..ca6924879324a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/index.ts @@ -6,3 +6,4 @@ */ export { Curations } from './curations'; +export { CurationCreation } from './curation_creation'; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts index 4ac79068a88f5..28896809bc81a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.test.ts @@ -50,6 +50,63 @@ describe('curations routes', () => { }); }); + describe('POST /api/app_search/engines/{engineName}/curations', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/curations', + }); + + registerCurationsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/curations/collection', + }); + }); + + describe('validates', () => { + it('with curation queries', () => { + const request = { + body: { + queries: ['a', 'b', 'c'], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('empty queries array', () => { + const request = { + body: { + queries: [], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('empty query strings', () => { + const request = { + body: { + queries: ['', '', ''], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('missing queries', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + describe('DELETE /api/app_search/engines/{engineName}/curations/{curationId}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts index 48bb2fc5cb823..2d7f09e1aeb8d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/curations.ts @@ -31,6 +31,23 @@ export function registerCurationsRoutes({ }) ); + router.post( + { + path: '/api/app_search/engines/{engineName}/curations', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + queries: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/curations/collection', + }) + ); + router.delete( { path: '/api/app_search/engines/{engineName}/curations/{curationId}', diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index eb7fbc9d590fa..9c806680f68a2 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -164,10 +164,12 @@ history records associated with specific saved object ids. ## API +Event Log plugin returns a service instance from setup() and client service from start() methods. + +### Setup ```typescript // IEvent is a TS type generated from the subset of ECS supported -// the NP plugin returns a service instance from setup() and start() export interface IEventLogService { registerProviderActions(provider: string, actions: string[]): void; isProviderActionRegistered(provider: string, action: string): boolean; @@ -237,6 +239,80 @@ properties `start`, `end`, and `duration` in the event. For example: It's anticipated that more "helper" methods like this will be provided in the future. +### Start +```typescript + +export interface IEventLogClientService { + getClient(request: KibanaRequest): IEventLogClient; +} + +export interface IEventLogClient { + findEventsBySavedObjectIds( + type: string, + ids: string[], + options?: Partial + ): Promise; +} +``` + +The plugin exposes an `IEventLogClientService` object to plugins that request it. +These plugins must call `getClient(request)` to get the event log client. + +## Experimental RESTful API + +Usage of the event log allows you to retrieve the events for a given saved object type by the specified set of IDs. +The following API is experimental and can change or be removed in a future release. + +### `GET /api/event_log/{type}/{id}/_find`: Get events for a given saved object type by the ID + +Collects event information from the event log for the selected saved object by type and ID. + +Params: + +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| +|id|The id of the saved object.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event fields returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| + +### `POST /api/event_log/{type}/_find`: Retrive events for a given saved object type by the IDs + +Collects event information from the event log for the selected saved object by type and by IDs. + +Params: + +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event field returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| + +Body: + +|Property|Description|Type| +|---|---|---| +|ids|The array ids of the saved object.|string array| ## Stored data @@ -303,4 +379,3 @@ For more relevant information on ILM, see: [getting started with ILM doc]: https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index-lifecycle-management.html [write index alias behavior]: https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-rollover-index.html#indices-rollover-is-write-index - diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index d95bc9cf736a6..abf6b3e1cbbd7 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -27,6 +27,7 @@ export const FLEET_SERVER_INDICES_VERSION = 1; export const FLEET_SERVER_INDICES = [ '.fleet-actions', '.fleet-agents', + '.fleet-artifacts', '.fleet-enrollment-api-keys', '.fleet-policies', '.fleet-policies-leader', diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts index 96e642ba9884e..310db24b8184d 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.test.ts @@ -14,42 +14,41 @@ import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.js import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; +import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; +import { FLEET_SERVER_INDICES } from '../../../common'; -const FLEET_INDEXES_MIGRATION_HASH = { +const FLEET_INDEXES_MIGRATION_HASH: Record = { '.fleet-actions': hash(EsFleetActionsIndex), '.fleet-agents': hash(ESFleetAgentIndex), + '.fleet-artifacts': hash(EsFleetArtifactsIndex), '.fleet-enrollment-apy-keys': hash(ESFleetEnrollmentApiKeysIndex), '.fleet-policies': hash(ESFleetPoliciesIndex), '.fleet-policies-leader': hash(ESFleetPoliciesLeaderIndex), '.fleet-servers': hash(ESFleetServersIndex), }; +const getIndexList = (returnAliases: boolean = false): string[] => { + const response = [...FLEET_SERVER_INDICES]; + + if (returnAliases) { + return response.sort(); + } + + return response.map((index) => `${index}_1`).sort(); +}; + describe('setupFleetServerIndexes ', () => { it('should create all the indices and aliases if nothings exists', async () => { const esMock = elasticsearchServiceMock.createInternalClient(); await setupFleetServerIndexes(esMock); const indexesCreated = esMock.indices.create.mock.calls.map((call) => call[0].index).sort(); - expect(indexesCreated).toEqual([ - '.fleet-actions_1', - '.fleet-agents_1', - '.fleet-enrollment-api-keys_1', - '.fleet-policies-leader_1', - '.fleet-policies_1', - '.fleet-servers_1', - ]); + expect(indexesCreated).toEqual(getIndexList()); const aliasesCreated = esMock.indices.updateAliases.mock.calls .map((call) => (call[0].body as any)?.actions[0].add.alias) .sort(); - expect(aliasesCreated).toEqual([ - '.fleet-actions', - '.fleet-agents', - '.fleet-enrollment-api-keys', - '.fleet-policies', - '.fleet-policies-leader', - '.fleet-servers', - ]); + expect(aliasesCreated).toEqual(getIndexList(true)); }); it('should not create any indices and create aliases if indices exists but not the aliases', async () => { @@ -63,7 +62,6 @@ describe('setupFleetServerIndexes ', () => { [params.index]: { mappings: { _meta: { - // @ts-expect-error migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], }, }, @@ -79,14 +77,7 @@ describe('setupFleetServerIndexes ', () => { .map((call) => (call[0].body as any)?.actions[0].add.alias) .sort(); - expect(aliasesCreated).toEqual([ - '.fleet-actions', - '.fleet-agents', - '.fleet-enrollment-api-keys', - '.fleet-policies', - '.fleet-policies-leader', - '.fleet-servers', - ]); + expect(aliasesCreated).toEqual(getIndexList(true)); }); it('should put new indices mapping if the mapping has been updated ', async () => { @@ -115,14 +106,7 @@ describe('setupFleetServerIndexes ', () => { .map((call) => call[0].index) .sort(); - expect(indexesMappingUpdated).toEqual([ - '.fleet-actions_1', - '.fleet-agents_1', - '.fleet-enrollment-api-keys_1', - '.fleet-policies-leader_1', - '.fleet-policies_1', - '.fleet-servers_1', - ]); + expect(indexesMappingUpdated).toEqual(getIndexList()); }); it('should not create any indices or aliases if indices and aliases already exists', async () => { @@ -137,7 +121,6 @@ describe('setupFleetServerIndexes ', () => { [params.index]: { mappings: { _meta: { - // @ts-expect-error migrationHash: FLEET_INDEXES_MIGRATION_HASH[params.index.replace(/_1$/, '')], }, }, diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts index 15672be756fe2..4b85f753740e3 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts @@ -16,10 +16,12 @@ import ESFleetPoliciesLeaderIndex from './elasticsearch/fleet_policies_leader.js import ESFleetServersIndex from './elasticsearch/fleet_servers.json'; import ESFleetEnrollmentApiKeysIndex from './elasticsearch/fleet_enrollment_api_keys.json'; import EsFleetActionsIndex from './elasticsearch/fleet_actions.json'; +import EsFleetArtifactsIndex from './elasticsearch/fleet_artifacts.json'; const FLEET_INDEXES: Array<[typeof FLEET_SERVER_INDICES[number], any]> = [ ['.fleet-actions', EsFleetActionsIndex], ['.fleet-agents', ESFleetAgentIndex], + ['.fleet-artifacts', EsFleetArtifactsIndex], ['.fleet-enrollment-api-keys', ESFleetEnrollmentApiKeysIndex], ['.fleet-policies', ESFleetPoliciesIndex], ['.fleet-policies-leader', ESFleetPoliciesLeaderIndex], diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json new file mode 100644 index 0000000000000..01a2c82b71861 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_artifacts.json @@ -0,0 +1,47 @@ +{ + "settings": {}, + "mappings": { + "dynamic": false, + "properties": { + "identifier": { + "type": "keyword" + }, + "compressionAlgorithm": { + "type": "keyword", + "index": false + }, + "encryptionAlgorithm": { + "type": "keyword", + "index": false + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "type": "long", + "index": false + }, + "decodedSha256": { + "type": "keyword", + "index": false + }, + "decodedSize": { + "type": "long", + "index": false + }, + "created": { + "type": "date", + "index": false + }, + "packageName": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "body": { + "type": "binary" + } + } + } +} diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index a9845c2315604..c61b431eed46d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -183,6 +183,13 @@ export const setup = async (arg?: { const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); + const showDataAllocationOptions = (phase: Phases) => () => { + act(() => { + find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); + }); + component.update(); + }; + const createMinAgeActions = (phase: Phases) => { return { hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), @@ -222,12 +229,10 @@ export const setup = async (arg?: { const createSearchableSnapshotActions = (phase: Phases) => { const fieldSelector = `searchableSnapshotField-${phase}`; const licenseCalloutSelector = `${fieldSelector}.searchableSnapshotDisabledDueToLicense`; - const rolloverCalloutSelector = `${fieldSelector}.searchableSnapshotFieldsNoRolloverCallout`; const toggleSelector = `${fieldSelector}.searchableSnapshotToggle`; const toggleSearchableSnapshot = createFormToggleAction(toggleSelector); return { - searchableSnapshotDisabledDueToRollover: () => exists(rolloverCalloutSelector), searchableSnapshotDisabled: () => exists(licenseCalloutSelector) && find(licenseCalloutSelector).props().disabled === true, searchableSnapshotsExists: () => exists(fieldSelector), @@ -379,6 +384,7 @@ export const setup = async (arg?: { }, warm: { enable: enable('warm'), + showDataAllocationOptions: showDataAllocationOptions('warm'), ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), @@ -390,6 +396,7 @@ export const setup = async (arg?: { }, cold: { enable: enable('cold'), + showDataAllocationOptions: showDataAllocationOptions('cold'), ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 7fe5c6f50d046..740aeebb852f1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -405,6 +405,7 @@ describe('', () => { await actions.cold.setMinAgeUnits('s'); await actions.cold.setDataAllocation('node_attrs'); await actions.cold.setSelectedNodeAttribute('test:123'); + await actions.cold.setSearchableSnapshot('my-repo'); await actions.cold.setReplicas('123'); await actions.cold.setFreeze(true); await actions.cold.setIndexPriority('123'); @@ -426,6 +427,9 @@ describe('', () => { }, }, "freeze": Object {}, + "searchable_snapshot": Object { + "snapshot_repository": "my-repo", + }, "set_priority": Object { "priority": 123, }, @@ -445,19 +449,6 @@ describe('', () => { } `); }); - - // Setting searchable snapshot field disables setting replicas so we test this separately - test('setting searchable snapshot', async () => { - const { actions } = testBed; - await actions.cold.enable(true); - await actions.cold.setSearchableSnapshot('my-repo'); - await actions.savePolicy(); - const latestRequest2 = server.requests[server.requests.length - 1]; - const entirePolicy2 = JSON.parse(JSON.parse(latestRequest2.requestBody).body); - expect(entirePolicy2.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( - 'my-repo' - ); - }); }); }); @@ -693,43 +684,52 @@ describe('', () => { expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); }); }); - }); - - describe('searchable snapshot', () => { describe('on cloud', () => { - describe('new policy', () => { + describe('using legacy data role config', () => { beforeEach(async () => { - // simulate creating a new policy - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, + // On cloud, even if there are data_* roles set, the default, recommended allocation option should not + // be available. + nodesByRoles: { data_hot: ['123'] }, + isUsingDeprecatedDataRoleConfig: true, }); httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + testBed = await setup({ + appServicesContext: { + cloud: { + isCloudEnabled: true, + }, + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); }); const { component } = testBed; component.update(); }); - test('defaults searchable snapshot to true on cloud', async () => { - const { find, actions } = testBed; - await actions.cold.enable(true); - expect( - find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] - ).toBe(true); + test('removes default, recommended option', async () => { + const { actions, find } = testBed; + await actions.warm.enable(true); + actions.warm.showDataAllocationOptions(); + + expect(find('defaultDataAllocationOption').exists()).toBeFalsy(); + expect(find('customDataAllocationOption').exists()).toBeTruthy(); + expect(find('noneDataAllocationOption').exists()).toBeTruthy(); + // Show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeTruthy(); }); }); - describe('existing policy', () => { + describe('using node roles', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, + nodesByRoles: { data_hot: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, }); httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); @@ -740,19 +740,34 @@ describe('', () => { const { component } = testBed; component.update(); }); - test('correctly sets snapshot repository default to "found-snapshots"', async () => { - const { actions } = testBed; + + test('should show recommended, custom and "off" options on cloud with data roles', async () => { + const { actions, find } = testBed; + + await actions.warm.enable(true); + actions.warm.showDataAllocationOptions(); + expect(find('defaultDataAllocationOption').exists()).toBeTruthy(); + expect(find('customDataAllocationOption').exists()).toBeTruthy(); + expect(find('noneDataAllocationOption').exists()).toBeTruthy(); + // We should not be showing the call-to-action for users to activate the cold tier on cloud + expect(find('cloudMissingColdTierCallout').exists()).toBeFalsy(); + // Do not show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeFalsy(); + }); + + test('should show cloud notice when cold tier nodes do not exist', async () => { + const { actions, find } = testBed; await actions.cold.enable(true); - await actions.cold.toggleSearchableSnapshot(true); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( - 'found-snapshots' - ); + expect(find('cloudMissingColdTierCallout').exists()).toBeTruthy(); + // Assert that other notices are not showing + expect(find('defaultAllocationNotice').exists()).toBeFalsy(); + expect(find('noNodeAttributesWarning').exists()).toBeFalsy(); }); }); }); + }); + + describe('searchable snapshot', () => { describe('on non-enterprise license', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -789,6 +804,64 @@ describe('', () => { expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); }); }); + + describe('on cloud', () => { + describe('new policy', () => { + beforeEach(async () => { + // simulate creating a new policy + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('defaults searchable snapshot to true on cloud', async () => { + const { find, actions } = testBed; + await actions.cold.enable(true); + expect( + find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] + ).toBe(true); + }); + }); + describe('existing policy', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data_hot: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('correctly sets snapshot repository default to "found-snapshots"', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'found-snapshots' + ); + }); + }); + }); }); describe('with rollover', () => { beforeEach(async () => { @@ -844,14 +917,15 @@ describe('', () => { const { component } = testBed; component.update(); }); - test('hiding and disabling searchable snapshot field', async () => { + test('hides fields in hot phase', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); - await actions.cold.enable(true); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + expect(actions.hot.shrinkExists()).toBeFalsy(); expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); - expect(actions.cold.searchableSnapshotDisabledDueToRollover()).toBeTruthy(); + expect(actions.hot.readonlyExists()).toBeFalsy(); }); test('hiding rollover tip on minimum age', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts index 113698fdf6df2..b02d190d10899 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -125,7 +125,7 @@ describe(' node allocation', () => { expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); }); - test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + test('when configuring warm phase shows default allocation notice when hot tier exists, but not warm tier', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, @@ -309,7 +309,7 @@ describe(' node allocation', () => { describe('on cloud', () => { describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { + test('should hide data tier option on cloud', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, // On cloud, if using legacy config there will not be any "data_*" roles set. @@ -331,10 +331,29 @@ describe(' node allocation', () => { expect(exists('customDataAllocationOption')).toBeTruthy(); expect(exists('noneDataAllocationOption')).toBeTruthy(); }); + + test('should ask users to migrate to node roles when on cloud using legacy data role', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + }); }); describe('with node role config', () => { - test('shows off, custom and data role options on cloud with data roles', async () => { + test('shows data role, custom and "off" options on cloud with data roles', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, @@ -372,7 +391,7 @@ describe(' node allocation', () => { await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(exists('cloudDataTierCallout')).toBeTruthy(); + expect(exists('cloudMissingColdTierCallout')).toBeTruthy(); // Assert that other notices are not showing expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 27aacef1a368b..1dbc30674eaa5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -8,12 +8,9 @@ import React, { FunctionComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; import { EuiTextColor } from '@elastic/eui'; -import { useFormData } from '../../../../../../shared_imports'; - import { useConfigurationIssues } from '../../../form'; import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; @@ -36,23 +33,12 @@ const i18nTexts = { }, }; -const formFieldPaths = { - enabled: '_meta.cold.enabled', - searchableSnapshot: 'phases.cold.actions.searchable_snapshot.snapshot_repository', -}; - export const ColdPhase: FunctionComponent = () => { const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); - const [formData] = useFormData({ - watch: [formFieldPaths.searchableSnapshot], - }); - - const showReplicasField = get(formData, formFieldPaths.searchableSnapshot) == null; - return ( }> - {showReplicasField && } + {/* Freeze section */} {!isUsingSearchableSnapshotInHotPhase && ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx index 4d3dbbba39037..351d6ac1c530b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx @@ -7,21 +7,38 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { - defaultMessage: 'Create a cold tier', + defaultMessage: 'Migrate to data tiers', }), body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + defaultMessage: 'Migrate your Elastic Cloud deployment to use data tiers.', }), + linkText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.linkToCloudDeploymentDescription', + { defaultMessage: 'View cloud deployment' } + ), }; -export const CloudDataTierCallout: FunctionComponent = () => { +interface Props { + linkToCloudDeployment?: string; +} + +/** + * A call-to-action for users to migrate to data tiers if their cluster is still running + * the deprecated node.data:true config. + */ +export const CloudDataTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { return ( - {i18nTexts.body} + {i18nTexts.body}{' '} + {Boolean(linkToCloudDeployment) && ( + + {i18nTexts.linkText} + + )} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx index 562267089051a..e43b750849774 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx @@ -11,8 +11,6 @@ import { EuiCallOut } from '@elastic/eui'; import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types'; -import { AllocationNodeRole } from '../../../../../../../lib'; - const i18nTextsNodeRoleToDataTier: Record = { data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { defaultMessage: 'hot', @@ -84,24 +82,13 @@ const i18nTexts = { interface Props { phase: PhaseWithAllocation; - targetNodeRole: AllocationNodeRole; + targetNodeRole: DataTierRole; } export const DefaultAllocationNotice: FunctionComponent = ({ phase, targetNodeRole }) => { - const content = - targetNodeRole === 'none' ? ( - - {i18nTexts.warning[phase].body} - - ) : ( - - {i18nTexts.notice[phase].body(targetNodeRole)} - - ); - - return content; + return ( + + {i18nTexts.notice[phase].body(targetNodeRole)} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx new file mode 100644 index 0000000000000..a194f3c07f900 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; + +const i18nTexts = { + warning: { + warm: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the warm tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, + cold: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the cold tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, + }, +}; + +interface Props { + phase: PhaseWithAllocation; +} + +export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => { + return ( + + {i18nTexts.warning[phase].body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index b3f57ac24e0d7..938e0a850f933 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -13,8 +13,12 @@ export { DataTierAllocation } from './data_tier_allocation'; export { DefaultAllocationNotice } from './default_allocation_notice'; +export { DefaultAllocationWarning } from './default_allocation_warning'; + export { NoNodeAttributesWarning } from './no_node_attributes_warning'; +export { MissingColdTierCallout } from './missing_cold_tier_callout'; + export { CloudDataTierCallout } from './cloud_data_tier_callout'; export { LoadingError } from './loading_error'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx new file mode 100644 index 0000000000000..21b8850e0b088 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', { + defaultMessage: 'Create a cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + }), + linkText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription', + { defaultMessage: 'View cloud deployment' } + ), +}; + +interface Props { + linkToCloudDeployment?: string; +} + +/** + * A call-to-action for users to activate their cold tier slider to provision cold tier nodes. + * This may need to be change when we have autoscaling enabled on a cluster because nodes may not + * yet exist, but will automatically be provisioned. + */ +export const MissingColdTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { + return ( + + {i18nTexts.body}{' '} + {Boolean(linkToCloudDeployment) && ( + + {i18nTexts.linkText} + + )} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index ad36039728f5c..7a660e0379a8d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -14,9 +14,7 @@ import { useKibana, useFormData } from '../../../../../../../shared_imports'; import { PhaseWithAllocation } from '../../../../../../../../common/types'; -import { getAvailableNodeRoleForPhase } from '../../../../../../lib/data_tiers'; - -import { isNodeRoleFirstPreference } from '../../../../../../lib'; +import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../../lib'; import { useLoadNodes } from '../../../../../../services/api'; @@ -25,7 +23,9 @@ import { DataTierAllocationType } from '../../../../types'; import { DataTierAllocation, DefaultAllocationNotice, + DefaultAllocationWarning, NoNodeAttributesWarning, + MissingColdTierCallout, CloudDataTierCallout, LoadingError, } from './components'; @@ -65,30 +65,48 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr ); const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; + const cloudDeploymentUrl = cloud?.cloudDeploymentUrl; const renderNotice = () => { switch (allocationType) { case 'node_roles': - if (isCloudEnabled && phase === 'cold') { - const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles; + /** + * We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role. + */ + if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) { const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; - if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) { + if (hasDataNodeRoles && hasNoNodesWithNodeRole) { // Tell cloud users they can deploy nodes on cloud. return ( <> - + ); } } + /** + * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting + * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even + * this fallback will not succeed. + */ const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); - if ( - allocationNodeRole === 'none' || - !isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { + if (allocationNodeRole === 'none') { + return ( + <> + + + + ); + } + + /** + * If we are able to fallback to a data tier that does not map to this phase, we show a notice informing the user that their + * data will not be assigned to a corresponding tier. + */ + if (!isNodeRoleFirstPreference(phase, allocationNodeRole)) { return ( <> @@ -106,6 +124,19 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr ); } + /** + * Special cloud case: when deprecated data role configuration is in use, it means that this deployment is not using + * the new node role based allocation. We drive users to the cloud console to migrate to node role based allocation + * in that case. + */ + if (isCloudEnabled && isUsingDeprecatedDataRoleConfig) { + return ( + <> + + + + ); + } break; default: return null; @@ -141,9 +172,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr hasNodeAttributes={hasNodeAttrs} phase={phase} nodes={nodesByAttributes} - disableDataTierOption={Boolean( - isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig - )} + disableDataTierOption={Boolean(isCloudEnabled && isUsingDeprecatedDataRoleConfig)} isLoading={isLoading} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 97e7d0bcc27de..1a78149521e63 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -52,7 +52,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => services: { cloud }, } = useKibana(); const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext(); - const { isUsingSearchableSnapshotInHotPhase, isUsingRollover } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; @@ -62,10 +62,8 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => const isColdPhase = phase === 'cold'; const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); const isDisabledInColdDueToHotPhase = isColdPhase && isUsingSearchableSnapshotInHotPhase; - const isDisabledInColdDueToRollover = isColdPhase && !isUsingRollover; - const isDisabled = - isDisabledDueToLicense || isDisabledInColdDueToHotPhase || isDisabledInColdDueToRollover; + const isDisabled = isDisabledDueToLicense || isDisabledInColdDueToHotPhase; const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => Boolean( @@ -294,20 +292,6 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => )} /> ); - } else if (isDisabledInColdDueToRollover) { - infoCallout = ( - - ); } return infoCallout ? ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index e5bf34890a4a7..577dab6804147 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -33,22 +33,22 @@ export const WarmPhase: FunctionComponent = () => { const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); return ( - - + + - {!isUsingSearchableSnapshotInHotPhase && } + {!isUsingSearchableSnapshotInHotPhase && } - {!isUsingSearchableSnapshotInHotPhase && } + {!isUsingSearchableSnapshotInHotPhase && } - + {/* Data tier allocation section */} - + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 27b3795c6731f..adfca9ad41b26 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx index f13ce33a44b3d..7cd6295cdcf40 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_dropdown.tsx @@ -6,12 +6,34 @@ */ import React, { useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertFlyout } from './alert_flyout'; import { useLinkProps } from '../../../hooks/use_link_props'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; + +const readOnlyUserTooltipContent = i18n.translate( + 'xpack.infra.logs.alertDropdown.readOnlyCreateAlertContent', + { + defaultMessage: 'Creating alerts requires more permissions in this application.', + } +); + +const readOnlyUserTooltipTitle = i18n.translate( + 'xpack.infra.logs.alertDropdown.readOnlyCreateAlertTitle', + { + defaultMessage: 'Read only', + } +); export const AlertDropdown = () => { + const { + services: { + application: { capabilities }, + }, + } = useKibanaContextForPlugin(); + const canCreateAlerts = capabilities?.logs?.save ?? false; const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const manageAlertsLinkProps = useLinkProps( @@ -34,7 +56,14 @@ export const AlertDropdown = () => { const menuItems = useMemo(() => { return [ - setFlyoutVisible(true)}> + setFlyoutVisible(true)} + toolTipContent={!canCreateAlerts ? readOnlyUserTooltipContent : undefined} + toolTipTitle={!canCreateAlerts ? readOnlyUserTooltipTitle : undefined} + > { /> , ]; - }, [manageAlertsLinkProps]); + }, [manageAlertsLinkProps, canCreateAlerts]); return ( <> diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index e1427bc96e7e0..c69c7ebff2b9e 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -66,7 +66,7 @@ export class LogStreamEmbeddable extends Embeddable { } const startTimestamp = datemathToEpochMillis(this.input.timeRange.from); - const endTimestamp = datemathToEpochMillis(this.input.timeRange.to); + const endTimestamp = datemathToEpochMillis(this.input.timeRange.to, 'up'); if (!startTimestamp || !endTimestamp) { return; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts index 3a0bc009bcfd0..3fa6bc065e7c1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts @@ -78,7 +78,9 @@ export const useWaffleViewState = () => { region: newState.region, legend: newState.legend, }); - if (newState.time) { + // if this is the "Default View" view, don't update the time range to the view's time range, + // this way it will use the global Kibana time or the default time already set + if (newState.time && newState.id !== '0') { setWaffleTimeState({ currentTime: newState.time, isAutoReloading: newState.autoReload, @@ -100,4 +102,5 @@ export type WaffleViewState = WaffleOptionsState & { time: number; autoReload: boolean; filterQuery: WaffleFiltersState; + id?: string; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index 424e456aa9dd8..eb5a4633d4fa9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -24,6 +24,7 @@ export interface MetricExplorerViewState { chartOptions: MetricsExplorerChartOptions; currentTimerange: MetricsExplorerTimeOptions; options: MetricsExplorerOptions; + id?: string; } export const useMetricsExplorerState = ( @@ -42,6 +43,7 @@ export const useMetricsExplorerState = ( setTimeRange, setOptions, } = useContext(MetricsExplorerOptionsContainer.Context); + const { loading, error, data, loadData } = useMetricsExplorerData( options, source, @@ -121,7 +123,11 @@ export const useMetricsExplorerState = ( setChartOptions(vs.chartOptions); } if (vs.currentTimerange) { - setTimeRange(vs.currentTimerange); + // if this is the "Default View" view, don't update the time range to the view's time range, + // this way it will use the global Kibana time or the default time already set + if (vs.id !== '0') { + setTimeRange(vs.currentTimerange); + } } if (vs.options) { setOptions(vs.options); 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 5f182203dd86c..37200f75d109c 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 @@ -68,7 +68,6 @@ describe('useMetricExplorerOptions', () => { expect(result.current.currentTimerange).toEqual(DEFAULT_TIMERANGE); expect(result.current.isAutoReloading).toEqual(false); expect(STORE.MetricsExplorerOptions).toEqual(JSON.stringify(DEFAULT_OPTIONS)); - expect(STORE.MetricsExplorerTimeRange).toEqual(JSON.stringify(DEFAULT_TIMERANGE)); }); it('should change the store when options update', () => { @@ -96,7 +95,6 @@ describe('useMetricExplorerOptions', () => { }); rerender(); expect(result.current.currentTimerange).toEqual(newTimeRange); - expect(STORE.MetricsExplorerTimeRange).toEqual(JSON.stringify(newTimeRange)); }); it('should load from store when available', () => { 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 06140dd976691..c1e5be94acc03 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 @@ -163,10 +163,7 @@ export const useMetricsExplorerOptions = () => { 'MetricsExplorerOptions', DEFAULT_OPTIONS ); - const [currentTimerange, setTimeRange] = useStateWithLocalStorage( - 'MetricsExplorerTimeRange', - defaultTimeRange - ); + const [currentTimerange, setTimeRange] = useState(defaultTimeRange); useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: currentTimerange.from, diff --git a/x-pack/plugins/maps/common/migrations/references.js b/x-pack/plugins/maps/common/migrations/references.js deleted file mode 100644 index ab8edbefb27c2..0000000000000 --- a/x-pack/plugins/maps/common/migrations/references.js +++ /dev/null @@ -1,107 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// Can not use public Layer classes to extract references since this logic must run in both client and server. - -import _ from 'lodash'; -import { SOURCE_TYPES } from '../constants'; - -function doesSourceUseIndexPattern(layerDescriptor) { - const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return ( - sourceType === SOURCE_TYPES.ES_GEO_GRID || - sourceType === SOURCE_TYPES.ES_SEARCH || - sourceType === SOURCE_TYPES.ES_PEW_PEW - ); -} - -export function extractReferences({ attributes, references = [] }) { - if (!attributes.layerListJSON) { - return { attributes, references }; - } - - const extractedReferences = []; - - const layerList = JSON.parse(attributes.layerListJSON); - layerList.forEach((layer, layerIndex) => { - // Extract index-pattern references from source descriptor - if (doesSourceUseIndexPattern(layer) && _.has(layer, 'sourceDescriptor.indexPatternId')) { - const refName = `layer_${layerIndex}_source_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: layer.sourceDescriptor.indexPatternId, - }); - delete layer.sourceDescriptor.indexPatternId; - layer.sourceDescriptor.indexPatternRefName = refName; - } - - // Extract index-pattern references from join - const joins = _.get(layer, 'joins', []); - joins.forEach((join, joinIndex) => { - if (_.has(join, 'right.indexPatternId')) { - const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: join.right.indexPatternId, - }); - delete join.right.indexPatternId; - join.right.indexPatternRefName = refName; - } - }); - }); - - return { - attributes: { - ...attributes, - layerListJSON: JSON.stringify(layerList), - }, - references: references.concat(extractedReferences), - }; -} - -function findReference(targetName, references) { - const reference = references.find((reference) => reference.name === targetName); - if (!reference) { - throw new Error(`Could not find reference "${targetName}"`); - } - return reference; -} - -export function injectReferences({ attributes, references }) { - if (!attributes.layerListJSON) { - return { attributes }; - } - - const layerList = JSON.parse(attributes.layerListJSON); - layerList.forEach((layer) => { - // Inject index-pattern references into source descriptor - if (doesSourceUseIndexPattern(layer) && _.has(layer, 'sourceDescriptor.indexPatternRefName')) { - const reference = findReference(layer.sourceDescriptor.indexPatternRefName, references); - layer.sourceDescriptor.indexPatternId = reference.id; - delete layer.sourceDescriptor.indexPatternRefName; - } - - // Inject index-pattern references into join - const joins = _.get(layer, 'joins', []); - joins.forEach((join) => { - if (_.has(join, 'right.indexPatternRefName')) { - const reference = findReference(join.right.indexPatternRefName, references); - join.right.indexPatternId = reference.id; - delete join.right.indexPatternRefName; - } - }); - }); - - return { - attributes: { - ...attributes, - layerListJSON: JSON.stringify(layerList), - }, - }; -} diff --git a/x-pack/plugins/maps/common/migrations/references.test.js b/x-pack/plugins/maps/common/migrations/references.test.ts similarity index 98% rename from x-pack/plugins/maps/common/migrations/references.test.js rename to x-pack/plugins/maps/common/migrations/references.test.ts index 3b8b7de441be4..5b749022bb62b 100644 --- a/x-pack/plugins/maps/common/migrations/references.test.js +++ b/x-pack/plugins/maps/common/migrations/references.test.ts @@ -128,7 +128,7 @@ describe('injectReferences', () => { const attributes = { title: 'my map', }; - expect(injectReferences({ attributes })).toEqual({ + expect(injectReferences({ attributes, references: [] })).toEqual({ attributes: { title: 'my map', }, diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts new file mode 100644 index 0000000000000..d48be6bd56fbe --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// Can not use public Layer classes to extract references since this logic must run in both client and server. + +import { SavedObjectReference } from '../../../../../src/core/types'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import { LayerDescriptor } from '../descriptor_types'; + +interface IndexPatternReferenceDescriptor { + indexPatternId?: string; + indexPatternRefName?: string; +} + +export function extractReferences({ + attributes, + references = [], +}: { + attributes: MapSavedObjectAttributes; + references?: SavedObjectReference[]; +}) { + if (!attributes.layerListJSON) { + return { attributes, references }; + } + + const extractedReferences: SavedObjectReference[] = []; + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layer, layerIndex) => { + // Extract index-pattern references from source descriptor + if (layer.sourceDescriptor && 'indexPatternId' in layer.sourceDescriptor) { + const sourceDescriptor = layer.sourceDescriptor as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_source_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + + // Extract index-pattern references from join + const joins = layer.joins ? layer.joins : []; + joins.forEach((join, joinIndex) => { + if ('indexPatternId' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + }); + }); + + return { + attributes: { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }, + references: references.concat(extractedReferences), + }; +} + +function findReference(targetName: string, references: SavedObjectReference[]) { + const reference = references.find(({ name }) => name === targetName); + if (!reference) { + throw new Error(`Could not find reference "${targetName}"`); + } + return reference; +} + +export function injectReferences({ + attributes, + references, +}: { + attributes: MapSavedObjectAttributes; + references: SavedObjectReference[]; +}) { + if (!attributes.layerListJSON) { + return { attributes }; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layer) => { + // Inject index-pattern references into source descriptor + if (layer.sourceDescriptor && 'indexPatternRefName' in layer.sourceDescriptor) { + const sourceDescriptor = layer.sourceDescriptor as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + + // Inject index-pattern references into join + const joins = layer.joins ? layer.joins : []; + joins.forEach((join) => { + if ('indexPatternRefName' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + }); + }); + + return { + attributes: { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }, + }; +} diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx new file mode 100644 index 0000000000000..16e4986f4d8a6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/create_source_editor.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { EmsTmsSourceConfig, TileServiceSelect } from './tile_service_select'; + +interface Props { + onTileSelect: (sourceConfig: EmsTmsSourceConfig) => void; +} + +interface State { + config?: EmsTmsSourceConfig; +} + +export class CreateSourceEditor extends Component { + state: State = {}; + + componentDidMount() { + this._onTileSelect({ id: null, isAutoSelect: true }); + } + + _onTileSelect = (config: EmsTmsSourceConfig) => { + this.setState({ config }); + this.props.onTileSelect(config); + }; + + render() { + return ( + + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 3cb707f377b70..859d8b95cef3f 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -7,14 +7,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiPanel } from '@elastic/eui'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { EMSTMSSource, getSourceTitle } from './ems_tms_source'; // @ts-ignore import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_layer'; -// @ts-ignore -import { TileServiceSelect } from './tile_service_select'; +import { EmsTmsSourceConfig } from './tile_service_select'; +import { CreateSourceEditor } from './create_source_editor'; import { getEMSSettings } from '../../../kibana_services'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WorldMapLayerIcon } from '../../layers/icons/world_map_layer_icon'; @@ -45,18 +44,14 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { }, icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: unknown) => { + const onSourceConfigChange = (sourceConfig: EmsTmsSourceConfig) => { const layerDescriptor = VectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), }); previewLayers([layerDescriptor]); }; - return ( - - - - ); + return ; }, title: getSourceTitle(), }; diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx similarity index 78% rename from x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx index 42ff1789df8f4..5f0f406d53e86 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.tsx @@ -5,17 +5,34 @@ * 2.0. */ -import React from 'react'; -import { EuiSelect, EuiFormRow } from '@elastic/eui'; +import React, { ChangeEvent, Component } from 'react'; +import { EuiSelect, EuiSelectOption, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { getEmsTmsServices } from '../../../meta'; import { getEmsUnavailableMessage } from '../../../components/ems_unavailable_message'; -import { i18n } from '@kbn/i18n'; export const AUTO_SELECT = 'auto_select'; -export class TileServiceSelect extends React.Component { - state = { +export interface EmsTmsSourceConfig { + id: string | null; + isAutoSelect: boolean; +} + +interface Props { + config?: EmsTmsSourceConfig; + onTileSelect: (sourceConfig: EmsTmsSourceConfig) => void; +} + +interface State { + emsTmsOptions: EuiSelectOption[]; + hasLoaded: boolean; +} + +export class TileServiceSelect extends Component { + private _isMounted = false; + + state: State = { emsTmsOptions: [], hasLoaded: false, }; @@ -51,7 +68,7 @@ export class TileServiceSelect extends React.Component { this.setState({ emsTmsOptions, hasLoaded: true }); }; - _onChange = (e) => { + _onChange = (e: ChangeEvent) => { const value = e.target.value; const isAutoSelect = value === AUTO_SELECT; this.props.onTileSelect({ @@ -63,9 +80,9 @@ export class TileServiceSelect extends React.Component { render() { const helpText = this.state.emsTmsOptions.length === 0 ? getEmsUnavailableMessage() : null; - let selectedId; + let selectedId: string | undefined; if (this.props.config) { - selectedId = this.props.config.isAutoSelect ? AUTO_SELECT : this.props.config.id; + selectedId = this.props.config.isAutoSelect ? AUTO_SELECT : this.props.config.id!; } return ( 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 7e15bfa9a340e..b1944f8136709 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -16,7 +16,6 @@ import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; -// @ts-expect-error import { extractReferences } from '../../common/migrations/references'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { @@ -69,7 +68,9 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const maybeMapByValueInput = state as EmbeddableStateWithType | MapByValueInput; if ((maybeMapByValueInput as MapByValueInput).attributes !== undefined) { - const { references } = extractReferences(maybeMapByValueInput); + const { references } = extractReferences({ + attributes: (maybeMapByValueInput as MapByValueInput).attributes, + }); return { state, references }; } diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 71b0b7f28a2ff..5f7c45b1b42d7 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -12,7 +12,6 @@ import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getMapEmbeddableDisplayName } from '../common/i18n_getters'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './kibana_services'; -// @ts-expect-error import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 8f9b529ae30f5..bf180c514c56f 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -24,7 +24,6 @@ import { import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; -// @ts-expect-error import { injectReferences } from '././../../common/migrations/references'; interface Settings { @@ -314,7 +313,10 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise { const savedObjectsWithIndexPatternIds = savedObjects.map((savedObject) => { - return injectReferences(savedObject); + return { + ...savedObject, + ...injectReferences(savedObject), + }; }); return layerLists.push(...getLayerLists(savedObjectsWithIndexPatternIds)); } diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index d494a8b529d2c..b037e72699dd6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -35,13 +35,19 @@ export const runTaskFnFactory: RunTaskFnFactory = function e return async function runTask(jobId, jobPayload, context, req) { const generateCsv = createGenerateCsv(logger); - const { panel, visType } = jobPayload; + const { panel } = jobPayload; - logger.debug(`Execute job generating [${visType}] csv`); + logger.debug(`Execute job generating saved search CSV`); const savedObjectsClient = context.core.savedObjects.client; const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const job = await getGenerateCsvParams(jobPayload, panel, savedObjectsClient, uiSettingsClient); + const job = await getGenerateCsvParams( + jobPayload, + panel, + savedObjectsClient, + uiSettingsClient, + logger + ); const elasticsearch = reporting.getElasticsearchService(); const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts index 8c189537ad5dd..fc6e092962d3b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts @@ -5,9 +5,12 @@ * 2.0. */ +import { createMockLevelLogger } from '../../../test_helpers'; import { JobParamsPanelCsv, SearchPanel } from '../types'; import { getGenerateCsvParams } from './get_csv_job'; +const logger = createMockLevelLogger(); + describe('Get CSV Job', () => { let mockJobParams: JobParamsPanelCsv; let mockSearchPanel: SearchPanel; @@ -42,7 +45,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -94,7 +98,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -149,7 +154,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -203,7 +209,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { @@ -275,7 +282,8 @@ describe('Get CSV Job', () => { mockJobParams, mockSearchPanel, mockSavedObjectsClient, - mockUiSettingsClient + mockUiSettingsClient, + logger ); expect(result).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index b4846ee3b4236..e4570816e26ff 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -8,6 +8,7 @@ import { IUiSettingsClient, SavedObjectsClientContract } from 'kibana/server'; import { EsQueryConfig } from 'src/plugins/data/server'; import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/server'; +import { LevelLogger } from '../../../lib'; import { TimeRangeParams } from '../../common'; import { GenerateCsvParams } from '../../csv/generate_csv'; import { @@ -44,7 +45,8 @@ export const getGenerateCsvParams = async ( jobParams: JobParamsPanelCsv, panel: SearchPanel, savedObjectsClient: SavedObjectsClientContract, - uiConfig: IUiSettingsClient + uiConfig: IUiSettingsClient, + logger: LevelLogger ): Promise => { let timerange: TimeRangeParams | null; if (jobParams.post?.timerange) { @@ -75,6 +77,14 @@ export const getGenerateCsvParams = async ( fields: indexPatternFields, } = indexPatternSavedObject; + if (!indexPatternFields || indexPatternFields.length === 0) { + logger.error( + new Error( + `No fields are selected in the saved search! Please select fields as columns in the saved search and try again.` + ) + ); + } + let payloadQuery: QueryFilter | undefined; let payloadSort: any[] = []; let docValueFields: DocValueFields[] | undefined; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 02342c895f077..323d267899bdf 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -7,8 +7,6 @@ import { cloneDeep, get, pick } from 'lodash'; -import { WEEK } from '../../../../../../../../src/plugins/es_ui_shared/public'; - import { validateId } from './validate_id'; import { validateIndexPattern } from './validate_index_pattern'; import { validateRollupIndex } from './validate_rollup_index'; @@ -66,7 +64,7 @@ export const stepIdToStepConfigMap = { // a few hours as they're being restarted. A delay of 1d would allow them that period to reboot // and the "expense" is pretty negligible in most cases: 1 day of extra non-rolled-up data. rollupDelay: '1d', - cronFrequency: WEEK, + cronFrequency: 'WEEK', fieldToPreferredValueMap: {}, }; diff --git a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js index fabd2a0be2454..1c54a42cbee63 100644 --- a/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/public/test/client_integration/job_create_logistics.test.js @@ -181,6 +181,11 @@ describe('Create Rollup Job, step 1: Logistics', () => { expect(options).toEqual(['minute', 'hour', 'day', 'week', 'month', 'year']); }); + it('should default to "WEEK"', () => { + const frequencySelect = find('cronFrequencySelect'); + expect(frequencySelect.props().value).toBe('WEEK'); + }); + describe('every minute', () => { it('should not have any additional configuration', () => { changeFrequency('MINUTE'); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 725a2eb9fea7b..79b912e082fdb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -5,9 +5,19 @@ * 2.0. */ -import { EntriesArray } from '../shared_imports'; +import { + CreateExceptionListItemSchema, + EntriesArray, + ExceptionListItemSchema, +} from '../shared_imports'; import { Type } from './schemas/common/schemas'; +export const hasLargeValueItem = ( + exceptionItems: Array +) => { + return exceptionItems.some((exceptionItem) => hasLargeValueList(exceptionItem.entries)); +}; + export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); return found.length > 0; diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 3ff719d267f40..96f83f1073fcc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -7,7 +7,7 @@ import { Client } from '@elastic/elasticsearch'; import seedrandom from 'seedrandom'; -import { KbnClient } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { EndpointDocGenerator, TreeOptions, Event } from './generate_data'; import { firstNonNullValue } from './models/ecs_safety_helpers'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx index bb4bd0f98949d..1e1e925a20ada 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; import styled from 'styled-components'; -import { Case } from '../../containers/types'; +import { Case, SubCase } from '../../containers/types'; import { CasesColumns } from './columns'; import { AssociationType } from '../../../../../case/common/api'; @@ -34,14 +34,25 @@ BasicTable.displayName = 'BasicTable'; export const getExpandedRowMap = ({ data, columns, + isModal, + onSubCaseClick, }: { data: Case[] | null; columns: CasesColumns[]; + isModal: boolean; + onSubCaseClick?: (theSubCase: SubCase) => void; }): ExpandedRowMap => { if (data == null) { return {}; } + const rowProps = (theSubCase: SubCase) => { + return { + ...(isModal && onSubCaseClick ? { onClick: () => onSubCaseClick(theSubCase) } : {}), + className: 'subCase', + }; + }; + return data.reduce((acc, curr) => { if (curr.subCases != null) { const subCases = curr.subCases.map((subCase, index) => ({ @@ -58,6 +69,7 @@ export const getExpandedRowMap = ({ data-test-subj={`sub-cases-table-${curr.id}`} itemId="id" items={subCases} + rowProps={rowProps} /> ), }; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index ce0fea07bf473..56dcf3bc28757 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -19,11 +19,12 @@ import { import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; import { isEmpty, memoize } from 'lodash/fp'; import styled, { css } from 'styled-components'; -import * as i18n from './translations'; +import classnames from 'classnames'; -import { CaseStatuses } from '../../../../../case/common/api'; +import * as i18n from './translations'; +import { CaseStatuses, CaseType } from '../../../../../case/common/api'; import { getCasesColumns } from './columns'; -import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers/types'; +import { Case, DeleteCase, FilterOptions, SortFieldCase, SubCase } from '../../containers/types'; import { useGetCases, UpdateCase } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useDeleteCases } from '../../containers/use_delete_cases'; @@ -58,6 +59,7 @@ import { getExpandedRowMap } from './expanded_row'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; `; + const FlexItemDivider = styled(EuiFlexItem)` ${({ theme }) => css` .euiFlexGroup--gutterMedium > &.euiFlexItem { @@ -75,6 +77,7 @@ const ProgressLoader = styled(EuiProgress)` z-index: ${theme.eui.euiZHeader}; `} `; + const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -86,19 +89,39 @@ const getSortField = (field: string): SortFieldCase => { const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any const BasicTable = styled(EuiBasicTable)` - .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { - padding: 8px 0 8px 32px; - } + ${({ theme }) => ` + .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { + padding: 8px 0 8px 32px; + } + + &.isModal .euiTableRow.isDisabled { + cursor: not-allowed; + background-color: ${theme.eui.euiTableHoverClickableColor}; + } + + &.isModal .euiTableRow.euiTableRow-isExpandedRow .euiTableRowCell, + &.isModal .euiTableRow.euiTableRow-isExpandedRow:hover { + background-color: transparent; + } + + &.isModal .euiTableRow.euiTableRow-isExpandedRow { + .subCase:hover { + background-color: ${theme.eui.euiTableHoverClickableColor}; + } + } + `} `; BasicTable.displayName = 'BasicTable'; interface AllCasesProps { - onRowClick?: (theCase?: Case) => void; + onRowClick?: (theCase?: Case | SubCase) => void; isModal?: boolean; userCanCrud: boolean; + disabledStatuses?: CaseStatuses[]; + disabledCases?: CaseType[]; } export const AllCases = React.memo( - ({ onRowClick, isModal = false, userCanCrud }) => { + ({ onRowClick, isModal = false, userCanCrud, disabledStatuses, disabledCases = [] }) => { const { navigateToApp } = useKibana().services.application; const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); const { actionLicense } = useGetActionLicense(); @@ -334,8 +357,10 @@ export const AllCases = React.memo( getExpandedRowMap({ columns: memoizedGetCasesColumns, data: data.cases, + isModal, + onSubCaseClick: onRowClick, }), - [data.cases, memoizedGetCasesColumns] + [data.cases, isModal, memoizedGetCasesColumns, onRowClick] ); const memoizedPagination = useMemo( @@ -356,6 +381,7 @@ export const AllCases = React.memo( () => ({ selectable: (theCase) => isEmpty(theCase.subCases), onSelectionChange: setSelectedCases, + selectableMessage: (selectable) => (!selectable ? i18n.SELECTABLE_MESSAGE_COLLECTIONS : ''), }), [setSelectedCases] ); @@ -377,7 +403,8 @@ export const AllCases = React.memo( return { 'data-test-subj': `cases-table-row-${theCase.id}`, - ...(isModal ? { onClick: onTableRowClick } : {}), + className: classnames({ isDisabled: theCase.type === CaseType.collection }), + ...(isModal && theCase.type !== CaseType.collection ? { onClick: onTableRowClick } : {}), }; }, [isModal, onRowClick] @@ -462,6 +489,7 @@ export const AllCases = React.memo( status: filterOptions.status, }} setFilterRefetch={setFilterRefetch} + disabledStatuses={disabledStatuses} /> {isCasesLoading && isDataEmpty ? (
@@ -530,6 +558,7 @@ export const AllCases = React.memo( rowProps={tableRowProps} selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined} sorting={sorting} + className={classnames({ isModal })} />
)} diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx index 785d4447c0acf..11d53b6609e74 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx @@ -61,4 +61,24 @@ describe('StatusFilter', () => { expect(onStatusChanged).toBeCalledWith('closed'); }); }); + + it('should disabled selected statuses', () => { + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click'); + + expect( + wrapper.find('button[data-test-subj="case-status-filter-open"]').prop('disabled') + ).toBeFalsy(); + + expect( + wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').prop('disabled') + ).toBeFalsy(); + + expect( + wrapper.find('button[data-test-subj="case-status-filter-closed"]').prop('disabled') + ).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx index 7fa0625229b48..41997d6f38421 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx @@ -14,9 +14,15 @@ interface Props { stats: Record; selectedStatus: CaseStatuses; onStatusChanged: (status: CaseStatuses) => void; + disabledStatuses?: CaseStatuses[]; } -const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatusChanged }) => { +const StatusFilterComponent: React.FC = ({ + stats, + selectedStatus, + onStatusChanged, + disabledStatuses = [], +}) => { const caseStatuses = Object.keys(statuses) as CaseStatuses[]; const options: Array> = caseStatuses.map((status) => ({ value: status, @@ -28,6 +34,7 @@ const StatusFilterComponent: React.FC = ({ stats, selectedStatus, onStatu {` (${stats[status]})`} ), + disabled: disabledStatuses.includes(status), 'data-test-subj': `case-status-filter-${status}`, })); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx index 1f7f1d1e0d487..61bbbac5a1e84 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx @@ -25,6 +25,7 @@ interface CasesTableFiltersProps { onFilterChanged: (filterOptions: Partial) => void; initial: FilterOptions; setFilterRefetch: (val: () => void) => void; + disabledStatuses?: CaseStatuses[]; } // Fix the width of the status dropdown to prevent hiding long text items @@ -50,6 +51,7 @@ const CasesTableFiltersComponent = ({ onFilterChanged, initial = defaultInitial, setFilterRefetch, + disabledStatuses, }: CasesTableFiltersProps) => { const [selectedReporters, setSelectedReporters] = useState( initial.reporters.map((r) => r.full_name ?? r.username ?? '') @@ -158,6 +160,7 @@ const CasesTableFiltersComponent = ({ selectedStatus={initial.status} onStatusChanged={onStatusChanged} stats={stats} + disabledStatuses={disabledStatuses} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx index 5e33736ce9c3a..95c534f7c1ede 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx @@ -16,7 +16,7 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../case/common/api'; +import { CaseStatuses, CaseType } from '../../../../../case/common/api'; import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import { Actions } from './actions'; @@ -73,19 +73,21 @@ const CaseActionBarComponent: React.FC = ({ ); return ( - + - - {i18n.STATUS} - - - - + {caseData.type !== CaseType.collection && ( + + {i18n.STATUS} + + + + + )} {title} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 7a5f6647a8dcf..5f9fb5b63d6eb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -29,6 +29,7 @@ import { connectorsMock } from '../../containers/configure/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; +import { CaseType } from '../../../../../case/common/api'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -201,6 +202,10 @@ describe('CaseView ', () => { .first() .text() ).toBe(data.description); + + expect( + wrapper.find('button[data-test-subj="case-view-status-action-button"]').first().text() + ).toBe('Mark in progress'); }); }); @@ -464,7 +469,7 @@ describe('CaseView ', () => { ); await waitFor(() => { wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click'); - expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + expect(fetchCaseUserActions).toBeCalledWith('1234', undefined); expect(fetchCase).toBeCalled(); }); }); @@ -547,8 +552,7 @@ describe('CaseView ', () => { }); }); - // TO DO fix when the useEffects in edit_connector are cleaned up - it.skip('should update connector', async () => { + it('should update connector', async () => { const wrapper = mount( @@ -752,4 +756,74 @@ describe('CaseView ', () => { }); }); }); + + describe('Collections', () => { + it('it does not allow the user to update the status', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="case-action-bar-wrapper"]').exists()).toBe(true); + expect(wrapper.find('button[data-test-subj="case-view-status"]').exists()).toBe(false); + expect(wrapper.find('[data-test-subj="user-actions"]').exists()).toBe(true); + expect( + wrapper.find('button[data-test-subj="case-view-status-action-button"]').exists() + ).toBe(false); + }); + }); + + it('it shows the push button when has data to push', async () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: true, + })); + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find('[data-test-subj="has-data-to-push-button"]').exists()).toBe(true); + }); + }); + + it('it does not show the horizontal rule when does NOT has data to push', async () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: false, + })); + + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="case-view-bottom-actions-horizontal-rule"]').exists() + ).toBe(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index e42431e55ee29..d0b7c34ab84fd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -17,7 +17,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses, CaseAttributes } from '../../../../../case/common/api'; +import { CaseStatuses, CaseAttributes, CaseType } from '../../../../../case/common/api'; import { Case, CaseConnector } from '../../containers/types'; import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to'; import { gutterTimeline } from '../../../common/lib/helpers'; @@ -213,9 +213,9 @@ export const CaseComponent = React.memo( const handleUpdateCase = useCallback( (newCase: Case) => { updateCase(newCase); - fetchCaseUserActions(newCase.id); + fetchCaseUserActions(caseId, subCaseId); }, - [updateCase, fetchCaseUserActions] + [updateCase, fetchCaseUserActions, caseId, subCaseId] ); const { loading: isLoadingConnectors, connectors } = useConnectors(); @@ -283,9 +283,9 @@ export const CaseComponent = React.memo( ); const handleRefresh = useCallback(() => { - fetchCaseUserActions(caseData.id); + fetchCaseUserActions(caseId, subCaseId); fetchCase(); - }, [caseData.id, fetchCase, fetchCaseUserActions]); + }, [caseId, fetchCase, fetchCaseUserActions, subCaseId]); const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); @@ -345,6 +345,7 @@ export const CaseComponent = React.memo( ); } }, [dispatch]); + return ( <> @@ -387,7 +388,7 @@ export const CaseComponent = React.memo( caseUserActions={caseUserActions} connectors={connectors} data={caseData} - fetchUserActions={fetchCaseUserActions.bind(null, caseData.id)} + fetchUserActions={fetchCaseUserActions.bind(null, caseId, subCaseId)} isLoadingDescription={isLoading && updateKey === 'description'} isLoadingUserActions={isLoadingUserActions} onShowAlertDetails={showAlert} @@ -395,22 +396,29 @@ export const CaseComponent = React.memo( updateCase={updateCase} userCanCrud={userCanCrud} /> - - - - + - - {hasDataToPush && ( - - {pushButton} - - )} - + {caseData.type !== CaseType.collection && ( + + + + )} + {hasDataToPush && ( + + {pushButton} + + )} + + )} )} @@ -465,6 +473,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = if (isError) { return null; } + if (isLoading) { return ( @@ -476,14 +485,16 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) = } return ( - + data && ( + + ) ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx index d5c90bd09a6db..b7fbaff288a2a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx @@ -47,7 +47,9 @@ const CaseParamsFields: React.FunctionComponent = ({ onCaseChanged, sel onCaseChanged(''); dispatchResetIsDeleted(); } - }, [isDeleted, dispatchResetIsDeleted, onCaseChanged]); + // onCaseChanged and/or dispatchResetIsDeleted causes re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDeleted]); useEffect(() => { if (!isLoading && !isError && data != null) { setCreatedCase(data); onCaseChanged(data.id); } - }, [data, isLoading, isError, onCaseChanged]); + // onCaseChanged causes re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, isLoading, isError]); return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx index b7a8a45edce5e..03000e8916617 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx @@ -35,19 +35,20 @@ export const useGetFieldsByIssueType = ({ }: Props): UseGetFieldsByIssueType => { const [isLoading, setIsLoading] = useState(true); const [fields, setFields] = useState({}); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector || !issueType) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getFieldsByIssueType({ http, signal: abortCtrl.current.signal, @@ -55,7 +56,7 @@ export const useGetFieldsByIssueType = ({ id: issueType, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setFields(res.data ?? {}); if (res.status && res.status === 'error') { @@ -66,22 +67,24 @@ export const useGetFieldsByIssueType = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.FIELDS_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, connector, issueType, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx index 4b60a9840c82b..3c35d315a2bcd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx @@ -35,27 +35,27 @@ export const useGetIssueTypes = ({ }: Props): UseGetIssueTypes => { const [isLoading, setIsLoading] = useState(true); const [issueTypes, setIssueTypes] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getIssueTypes({ http, signal: abortCtrl.current.signal, connectorId: connector.id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); const asOptions = (res.data ?? []).map((type) => ({ text: type.name ?? '', @@ -71,25 +71,29 @@ export const useGetIssueTypes = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.ISSUE_TYPES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; - }, [http, connector, toastNotifications, handleIssueType]); + // handleIssueType unmounts the component at init causing the request to be aborted + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, connector, toastNotifications]); return { issueTypes, diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx index 170cf2b53395e..b44b0558f1536 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx @@ -36,20 +36,20 @@ export const useGetIssues = ({ }: Props): UseGetIssues => { const [isLoading, setIsLoading] = useState(false); const [issues, setIssues] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = debounce(500, async () => { if (!actionConnector || isEmpty(query)) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getIssues({ http, signal: abortCtrl.current.signal, @@ -57,7 +57,7 @@ export const useGetIssues = ({ title: query ?? '', }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setIssues(res.data ?? []); if (res.status && res.status === 'error') { @@ -68,22 +68,24 @@ export const useGetIssues = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.ISSUES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.ISSUES_API_ERROR, + text: error.message, + }); + } } } }); + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, actionConnector, toastNotifications, query]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx index 89b42b1a88c1e..6c70286426168 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx @@ -35,10 +35,10 @@ export const useGetSingleIssue = ({ }: Props): UseGetSingleIssue => { const [isLoading, setIsLoading] = useState(false); const [issue, setIssue] = useState(null); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!actionConnector || !id) { setIsLoading(false); @@ -55,7 +55,7 @@ export const useGetSingleIssue = ({ id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setIssue(res.data ?? null); if (res.status && res.status === 'error') { @@ -66,22 +66,24 @@ export const useGetSingleIssue = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.GET_ISSUE_API_ERROR(id), - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.GET_ISSUE_API_ERROR(id), + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, actionConnector, id, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx index 99964f466058f..34cbb0a69b0f4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx @@ -34,27 +34,27 @@ export const useGetIncidentTypes = ({ }: Props): UseGetIncidentTypes => { const [isLoading, setIsLoading] = useState(true); const [incidentTypes, setIncidentTypes] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getIncidentTypes({ http, signal: abortCtrl.current.signal, connectorId: connector.id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setIncidentTypes(res.data ?? []); if (res.status && res.status === 'error') { @@ -65,22 +65,24 @@ export const useGetIncidentTypes = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.INCIDENT_TYPES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, connector, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx index 0a71891ae41b2..5b44c6b4a32b2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx @@ -7,9 +7,9 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; import { getSeverity } from './api'; import * as i18n from './translations'; -import { ActionConnector } from '../../../containers/types'; type Severity = Array<{ id: number; name: string }>; @@ -31,26 +31,26 @@ export const useGetSeverity = ({ http, toastNotifications, connector }: Props): const [isLoading, setIsLoading] = useState(true); const [severity, setSeverity] = useState([]); const abortCtrl = useRef(new AbortController()); + const didCancel = useRef(false); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getSeverity({ http, signal: abortCtrl.current.signal, connectorId: connector.id, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setSeverity(res.data ?? []); @@ -62,22 +62,24 @@ export const useGetSeverity = ({ http, toastNotifications, connector }: Props): } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.SEVERITY_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; }, [http, connector, toastNotifications]); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx index 16e905bdabfee..a979f96d84ab2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx @@ -37,20 +37,20 @@ export const useGetChoices = ({ }: UseGetChoicesProps): UseGetChoices => { const [isLoading, setIsLoading] = useState(false); const [choices, setChoices] = useState([]); + const didCancel = useRef(false); const abortCtrl = useRef(new AbortController()); useEffect(() => { - let didCancel = false; const fetchData = async () => { if (!connector) { setIsLoading(false); return; } - abortCtrl.current = new AbortController(); - setIsLoading(true); - try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + const res = await getChoices({ http, signal: abortCtrl.current.signal, @@ -58,7 +58,7 @@ export const useGetChoices = ({ fields, }); - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); setChoices(res.data ?? []); if (res.status && res.status === 'error') { @@ -71,22 +71,24 @@ export const useGetChoices = ({ } } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { setIsLoading(false); - toastNotifications.addDanger({ - title: i18n.CHOICES_API_ERROR, - text: error.message, - }); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: error.message, + }); + } } } }; + didCancel.current = false; abortCtrl.current.abort(); fetchData(); return () => { - didCancel = true; - setIsLoading(false); + didCancel.current = true; abortCtrl.current.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx index 842fe9e00ab39..d5883b7b88cd0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx @@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> @@ -55,10 +57,10 @@ jest.mock('../create/submit_button', () => { }); const onCloseFlyout = jest.fn(); -const onCaseCreated = jest.fn(); +const onSuccess = jest.fn(); const defaultProps = { onCloseFlyout, - onCaseCreated, + onSuccess, }; describe('CreateCaseFlyout', () => { @@ -97,7 +99,7 @@ describe('CreateCaseFlyout', () => { const props = wrapper.find('FormContext').props(); expect(props).toEqual( expect.objectContaining({ - onSuccess: onCaseCreated, + onSuccess, }) ); }); @@ -110,6 +112,6 @@ describe('CreateCaseFlyout', () => { ); wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click'); - expect(onCaseCreated).toHaveBeenCalledWith({ id: 'case-id' }); + expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx index cb3436f6ba3bc..e7bb0b25f391f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx @@ -17,7 +17,8 @@ import * as i18n from '../../translations'; export interface CreateCaseModalProps { onCloseFlyout: () => void; - onCaseCreated: (theCase: Case) => void; + onSuccess: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; } const Container = styled.div` @@ -40,7 +41,8 @@ const FormWrapper = styled.div` `; const CreateCaseFlyoutComponent: React.FC = ({ - onCaseCreated, + onSuccess, + afterCaseCreated, onCloseFlyout, }) => { return ( @@ -52,7 +54,7 @@ const CreateCaseFlyoutComponent: React.FC = ({ - + diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx index 8236ab7b19d27..1e512ef5ffabd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx @@ -98,6 +98,7 @@ const fillForm = (wrapper: ReactWrapper) => { describe('Create case', () => { const fetchTags = jest.fn(); const onFormSubmitSuccess = jest.fn(); + const afterCaseCreated = jest.fn(); beforeEach(() => { jest.resetAllMocks(); @@ -593,4 +594,89 @@ describe('Create case', () => { }); }); }); + + it(`it should call afterCaseCreated`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(afterCaseCreated).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should call callbacks in correct order`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(postCase).toHaveBeenCalled(); + expect(afterCaseCreated).toHaveBeenCalled(); + expect(pushCaseToExternalService).toHaveBeenCalled(); + expect(onFormSubmitSuccess).toHaveBeenCalled(); + }); + + const postCaseOrder = postCase.mock.invocationCallOrder[0]; + const afterCaseOrder = afterCaseCreated.mock.invocationCallOrder[0]; + const pushCaseToExternalServiceOrder = pushCaseToExternalService.mock.invocationCallOrder[0]; + const onFormSubmitSuccessOrder = onFormSubmitSuccess.mock.invocationCallOrder[0]; + + expect( + postCaseOrder < afterCaseOrder && + postCaseOrder < pushCaseToExternalServiceOrder && + postCaseOrder < onFormSubmitSuccessOrder + ).toBe(true); + + expect( + afterCaseOrder < pushCaseToExternalServiceOrder && afterCaseOrder < onFormSubmitSuccessOrder + ).toBe(true); + + expect(pushCaseToExternalServiceOrder < onFormSubmitSuccessOrder).toBe(true); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 83b8870ab597d..26203d7268fd3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -32,13 +32,15 @@ const initialCaseValue: FormProps = { interface Props { caseType?: CaseType; - onSuccess?: (theCase: Case) => void; + onSuccess?: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; } export const FormContext: React.FC = ({ caseType = CaseType.individual, children, onSuccess, + afterCaseCreated, }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); @@ -72,6 +74,10 @@ export const FormContext: React.FC = ({ settings: { syncAlerts }, }); + if (afterCaseCreated && updatedCase) { + await afterCaseCreated(updatedCase); + } + if (updatedCase?.id && dataConnectorId !== 'none') { await pushCaseToExternalService({ caseId: updatedCase.id, @@ -80,11 +86,11 @@ export const FormContext: React.FC = ({ } if (onSuccess && updatedCase) { - onSuccess(updatedCase); + await onSuccess(updatedCase); } } }, - [caseType, connectors, postCase, onSuccess, pushCaseToExternalService] + [caseType, connectors, postCase, onSuccess, pushCaseToExternalService, afterCaseCreated] ); const { form } = useForm({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index b7d162bd92761..9f904350b772e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -41,7 +41,7 @@ const InsertTimeline = () => { export const Create = React.memo(() => { const history = useHistory(); const onSuccess = useCallback( - ({ id }) => { + async ({ id }) => { history.push(getCaseDetailsUrl({ id })); }, [history] diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx index ab30fe2979b9e..22d72429836de 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx @@ -42,7 +42,7 @@ describe('StatusActionButton', () => { expect( wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') - ).toBe('folderClosed'); + ).toBe('folderCheck'); }); it('it renders the correct button icon: status closed', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/config.ts b/x-pack/plugins/security_solution/public/cases/components/status/config.ts index d811db43df814..0eebef39859c7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/config.ts @@ -83,7 +83,7 @@ export const statuses: Statuses = { [CaseStatuses.closed]: { color: 'default', label: i18n.CLOSED, - icon: 'folderClosed' as const, + icon: 'folderCheck' as const, actions: { bulk: { title: i18n.BULK_ACTION_CLOSE_SELECTED, diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index aa1305f1f655c..b3302a05cfcb2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -30,12 +30,18 @@ jest.mock('../../../common/components/toasters', () => { jest.mock('../all_cases', () => { return { - AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => { + AllCases: ({ onRowClick }: { onRowClick: (theCase: Partial) => void }) => { return ( @@ -49,18 +55,25 @@ jest.mock('../create/form_context', () => { FormContext: ({ children, onSuccess, + afterCaseCreated, }: { children: ReactNode; - onSuccess: (theCase: Partial) => void; + onSuccess: (theCase: Partial) => Promise; + afterCaseCreated: (theCase: Partial) => Promise; }) => { return ( <> @@ -212,11 +225,43 @@ describe('AddToCaseAction', () => { }); }); - it('navigates to case view', async () => { + it('navigates to case view when attach to a new case', async () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + + expect(mockDispatchToaster).toHaveBeenCalled(); + const toast = mockDispatchToaster.mock.calls[0][0].toast; + + const toastWrapper = mount( + {}} /> + ); + + toastWrapper + .find('[data-test-subj="toaster-content-case-view-link"]') + .first() + .simulate('click'); + + expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' }); + }); + + it('navigates to case view when attach to an existing case', async () => { usePostCommentMock.mockImplementation(() => { return { ...defaultPostComment, - postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => updateCase()), + postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => { + updateCase({ + id: 'selected-case', + title: 'the selected case', + settings: { syncAlerts: true }, + }); + }), }; }); @@ -227,8 +272,8 @@ describe('AddToCaseAction', () => { ); wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click'); - wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="all-cases-modal-button"]`).first().simulate('click'); expect(mockDispatchToaster).toHaveBeenCalled(); const toast = mockDispatchToaster.mock.calls[0][0].toast; @@ -242,6 +287,8 @@ describe('AddToCaseAction', () => { .first() .simulate('click'); - expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/new-case' }); + expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', { + path: '/selected-case', + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index aa9cec2d6b5b1..3000551dd3c07 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -15,7 +15,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { CommentType } from '../../../../../case/common/api'; +import { CommentType, CaseStatuses } from '../../../../../case/common/api'; import { Ecs } from '../../../../common/ecs'; import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item'; import { usePostComment } from '../../containers/use_post_comment'; @@ -70,9 +70,9 @@ const AddToCaseActionComponent: React.FC = ({ } = useControl(); const attachAlertToCase = useCallback( - (theCase: Case) => { + async (theCase: Case, updateCase?: (newCase: Case) => void) => { closeCaseFlyoutOpen(); - postComment({ + await postComment({ caseId: theCase.id, data: { type: CommentType.alert, @@ -83,14 +83,19 @@ const AddToCaseActionComponent: React.FC = ({ name: rule?.name != null ? rule.name[0] : null, }, }, - updateCase: () => - dispatchToaster({ - type: 'addToaster', - toast: createUpdateSuccessToaster(theCase, onViewCaseClick), - }), + updateCase, }); }, - [closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule, dispatchToaster, onViewCaseClick] + [closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule] + ); + + const onCaseSuccess = useCallback( + async (theCase: Case) => + dispatchToaster({ + type: 'addToaster', + toast: createUpdateSuccessToaster(theCase, onViewCaseClick), + }), + [dispatchToaster, onViewCaseClick] ); const onCaseClicked = useCallback( @@ -105,12 +110,13 @@ const AddToCaseActionComponent: React.FC = ({ return; } - attachAlertToCase(theCase); + attachAlertToCase(theCase, onCaseSuccess); }, - [attachAlertToCase, openCaseFlyoutOpen] + [attachAlertToCase, onCaseSuccess, openCaseFlyoutOpen] ); const { modal: allCasesModal, openModal: openAllCaseModal } = useAllCasesModal({ + disabledStatuses: [CaseStatuses.closed], onRowClick: onCaseClicked, }); @@ -183,7 +189,11 @@ const AddToCaseActionComponent: React.FC = ({ {isCreateCaseFlyoutOpen && ( - + )} {allCasesModal} diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index eda8ed8cdfbcd..e1d6baa6e630a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -6,36 +6,52 @@ */ import React, { memo } from 'react'; +import styled from 'styled-components'; import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; -import { Case } from '../../containers/types'; +import { CaseStatuses } from '../../../../../case/common/api'; +import { Case, SubCase } from '../../containers/types'; import { AllCases } from '../all_cases'; import * as i18n from './translations'; export interface AllCasesModalProps { isModalOpen: boolean; onCloseCaseModal: () => void; - onRowClick: (theCase?: Case) => void; + onRowClick: (theCase?: Case | SubCase) => void; + disabledStatuses?: CaseStatuses[]; } +const Modal = styled(EuiModal)` + ${({ theme }) => ` + width: ${theme.eui.euiBreakpoints.l}; + max-width: ${theme.eui.euiBreakpoints.l}; + `} +`; + const AllCasesModalComponent: React.FC = ({ isModalOpen, onCloseCaseModal, onRowClick, + disabledStatuses, }) => { const userPermissions = useGetUserSavedObjectPermissions(); const userCanCrud = userPermissions?.crud ?? false; return isModalOpen ? ( - + {i18n.SELECT_CASE_TITLE} - + - + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx index 576f942a36a8f..57bb39a1ab50f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.test.tsx @@ -123,7 +123,7 @@ describe('useAllCasesModal', () => { }); const modal = result.current.modal; - render(<>{modal}); + render({modal}); act(() => { userEvent.click(screen.getByText('case-row')); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx index 79b490c1962da..52b8ebe0210c0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx @@ -6,11 +6,13 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { Case } from '../../containers/types'; +import { CaseStatuses } from '../../../../../case/common/api'; +import { Case, SubCase } from '../../containers/types'; import { AllCasesModal } from './all_cases_modal'; export interface UseAllCasesModalProps { - onRowClick: (theCase?: Case) => void; + onRowClick: (theCase?: Case | SubCase) => void; + disabledStatuses?: CaseStatuses[]; } export interface UseAllCasesModalReturnedValues { @@ -22,12 +24,13 @@ export interface UseAllCasesModalReturnedValues { export const useAllCasesModal = ({ onRowClick, + disabledStatuses, }: UseAllCasesModalProps): UseAllCasesModalReturnedValues => { const [isModalOpen, setIsModalOpen] = useState(false); const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); const onClick = useCallback( - (theCase?: Case) => { + (theCase?: Case | SubCase) => { closeModal(); onRowClick(theCase); }, @@ -41,6 +44,7 @@ export const useAllCasesModal = ({ isModalOpen={isModalOpen} onCloseCaseModal={closeModal} onRowClick={onClick} + disabledStatuses={disabledStatuses} /> ), isModalOpen, @@ -48,7 +52,7 @@ export const useAllCasesModal = ({ openModal, onRowClick, }), - [isModalOpen, closeModal, onClick, openModal, onRowClick] + [isModalOpen, closeModal, onClick, disabledStatuses, openModal, onRowClick] ); return state; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx index 0e04acb013b2d..08fca0cc6e009 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.test.tsx @@ -20,14 +20,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 2806e358fceee..3e11ee526839c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -19,7 +19,7 @@ import { CaseType } from '../../../../../case/common/api'; export interface CreateCaseModalProps { isModalOpen: boolean; onCloseCaseModal: () => void; - onSuccess: (theCase: Case) => void; + onSuccess: (theCase: Case) => Promise; caseType?: CaseType; } diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx index 9966cf75351dd..5174c03e56e0b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.test.tsx @@ -25,14 +25,16 @@ jest.mock('../create/form_context', () => { onSuccess, }: { children: ReactNode; - onSuccess: ({ id }: { id: string }) => void; + onSuccess: ({ id }: { id: string }) => Promise; }) => { return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 3dc852a19e73f..1cef63ae9cfbf 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -29,7 +29,7 @@ export const useCreateCaseModal = ({ const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); const onSuccess = useCallback( - (theCase) => { + async (theCase) => { onCaseCreated(theCase); closeModal(); }, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index c5d3ef1893ad7..056add32add82 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -55,6 +55,9 @@ describe('UserActionTree ', () => { useFormMock.mockImplementation(() => ({ form: formHookMock })); useFormDataMock.mockImplementation(() => [{ content: sampleData.content, comment: '' }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + jest + .spyOn(routeData, 'useParams') + .mockReturnValue({ detailName: 'case-id', subCaseId: 'sub-case-id' }); }); it('Loading spinner when user actions loading and displays fullName/username', () => { @@ -289,7 +292,8 @@ describe('UserActionTree ', () => { ).toEqual(false); expect(patchComment).toBeCalledWith({ commentUpdate: sampleData.content, - caseId: props.data.id, + caseId: 'case-id', + subCaseId: 'sub-case-id', commentId: props.data.comments[0].id, fetchUserActions, updateCase, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 2a9f99465251b..cf68d07859ced 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -122,7 +122,11 @@ export const UserActionTree = React.memo( userCanCrud, onShowAlertDetails, }: UserActionTreeProps) => { - const { commentId, subCaseId } = useParams<{ commentId?: string; subCaseId?: string }>(); + const { detailName: caseId, commentId, subCaseId } = useParams<{ + detailName: string; + commentId?: string; + subCaseId?: string; + }>(); const handlerTimeoutId = useRef(0); const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); @@ -149,15 +153,16 @@ export const UserActionTree = React.memo( const handleSaveComment = useCallback( ({ id, version }: { id: string; version: string }, content: string) => { patchComment({ - caseId: caseData.id, + caseId, commentId: id, commentUpdate: content, fetchUserActions, version, updateCase, + subCaseId, }); }, - [caseData.id, fetchUserActions, patchComment, updateCase] + [caseId, fetchUserActions, patchComment, subCaseId, updateCase] ); const handleOutlineComment = useCallback( @@ -223,7 +228,7 @@ export const UserActionTree = React.memo( const MarkdownNewComment = useMemo( () => ( ), - [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId, subCaseId] + [caseId, userCanCrud, handleUpdate, handleManageMarkdownEditId, subCaseId] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx index ff5762b8476de..3590fffdef5b2 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_action_types.tsx @@ -22,25 +22,25 @@ export const useActionTypes = (): UseActionTypesResponse => { const [, dispatchToaster] = useStateToaster(); const [loading, setLoading] = useState(true); const [actionTypes, setActionTypes] = useState([]); - const didCancel = useRef(false); - const abortCtrl = useRef(new AbortController()); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const queryFirstTime = useRef(true); const refetchActionTypes = useCallback(async () => { try { setLoading(true); - didCancel.current = false; - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); - const res = await fetchActionTypes({ signal: abortCtrl.current.signal }); + const res = await fetchActionTypes({ signal: abortCtrlRef.current.signal }); - if (!didCancel.current) { + if (!isCancelledRef.current) { setLoading(false); setActionTypes(res); } } catch (error) { - if (!didCancel.current) { + if (!isCancelledRef.current) { setLoading(false); setActionTypes([]); errorToToaster({ @@ -59,8 +59,8 @@ export const useActionTypes = (): UseActionTypesResponse => { } return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); queryFirstTime.current = true; }; }, [refetchActionTypes]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index cc8c93fc990eb..21d1832796ba8 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect, useCallback, useReducer } from 'react'; +import { useEffect, useCallback, useReducer, useRef } from 'react'; import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; import { @@ -207,129 +207,128 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }, []); const [, dispatchToaster] = useStateToaster(); + const isCancelledRefetchRef = useRef(false); + const abortCtrlRefetchRef = useRef(new AbortController()); - const refetchCaseConfigure = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); + const isCancelledPersistRef = useRef(false); + const abortCtrlPersistRef = useRef(new AbortController()); - const fetchCaseConfiguration = async () => { - try { - setLoading(true); - const res = await getCaseConfigure({ signal: abortCtrl.signal }); - if (!didCancel) { - if (res != null) { - setConnector(res.connector); - if (setClosureType != null) { - setClosureType(res.closureType); - } - setVersion(res.version); - setMappings(res.mappings); + const refetchCaseConfigure = useCallback(async () => { + try { + isCancelledRefetchRef.current = false; + abortCtrlRefetchRef.current.abort(); + abortCtrlRefetchRef.current = new AbortController(); - if (!state.firstLoad) { - setFirstLoad(true); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - closureType: res.closureType, - connector: { - ...res.connector, - }, - }); - } - } - if (res.error != null) { - errorToToaster({ - dispatchToaster, - error: new Error(res.error), - title: i18n.ERROR_TITLE, + setLoading(true); + const res = await getCaseConfigure({ signal: abortCtrlRefetchRef.current.signal }); + + if (!isCancelledRefetchRef.current) { + if (res != null) { + setConnector(res.connector); + if (setClosureType != null) { + setClosureType(res.closureType); + } + setVersion(res.version); + setMappings(res.mappings); + + if (!state.firstLoad) { + setFirstLoad(true); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connector: { + ...res.connector, + }, }); } } - setLoading(false); + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } } - } catch (error) { - if (!didCancel) { - setLoading(false); + setLoading(false); + } + } catch (error) { + if (!isCancelledRefetchRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ dispatchToaster, error: error.body && error.body.message ? new Error(error.body.message) : error, title: i18n.ERROR_TITLE, }); } + setLoading(false); } - }; - - fetchCaseConfiguration(); - - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.firstLoad]); const persistCaseConfigure = useCallback( async ({ connector, closureType }: ConnectorConfiguration) => { - let didCancel = false; - const abortCtrl = new AbortController(); - const saveCaseConfiguration = async () => { - try { - setPersistLoading(true); - const connectorObj = { - connector, - closure_type: closureType, - }; - const res = - state.version.length === 0 - ? await postCaseConfigure(connectorObj, abortCtrl.signal) - : await patchCaseConfigure( - { - ...connectorObj, - version: state.version, - }, - abortCtrl.signal - ); - if (!didCancel) { - setConnector(res.connector); - if (setClosureType) { - setClosureType(res.closureType); - } - setVersion(res.version); - setMappings(res.mappings); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - closureType: res.closureType, - connector: { - ...res.connector, + try { + isCancelledPersistRef.current = false; + abortCtrlPersistRef.current.abort(); + abortCtrlPersistRef.current = new AbortController(); + setPersistLoading(true); + + const connectorObj = { + connector, + closure_type: closureType, + }; + + const res = + state.version.length === 0 + ? await postCaseConfigure(connectorObj, abortCtrlPersistRef.current.signal) + : await patchCaseConfigure( + { + ...connectorObj, + version: state.version, }, - }); - } - if (res.error != null) { - errorToToaster({ - dispatchToaster, - error: new Error(res.error), - title: i18n.ERROR_TITLE, - }); - } - displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); - setPersistLoading(false); + abortCtrlPersistRef.current.signal + ); + + if (!isCancelledPersistRef.current) { + setConnector(res.connector); + if (setClosureType) { + setClosureType(res.closureType); + } + setVersion(res.version); + setMappings(res.mappings); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connector: { + ...res.connector, + }, + }); } - } catch (error) { - if (!didCancel) { - setConnector(state.currentConfiguration.connector); - setPersistLoading(false); + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); + setPersistLoading(false); + } + } catch (error) { + if (!isCancelledPersistRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); } + setConnector(state.currentConfiguration.connector); + setPersistLoading(false); } - }; - saveCaseConfiguration(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } }, [ dispatchToaster, @@ -345,6 +344,12 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { useEffect(() => { refetchCaseConfigure(); + return () => { + isCancelledRefetchRef.current = true; + abortCtrlRefetchRef.current.abort(); + isCancelledPersistRef.current = true; + abortCtrlPersistRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx index d21e50902ca83..338d04f702c63 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { useStateToaster, errorToToaster } from '../../../common/components/toasters'; import * as i18n from '../translations'; @@ -22,40 +22,45 @@ export const useConnectors = (): UseConnectorsResponse => { const [, dispatchToaster] = useStateToaster(); const [loading, setLoading] = useState(true); const [connectors, setConnectors] = useState([]); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const refetchConnectors = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const getConnectors = async () => { - try { - setLoading(true); - const res = await fetchConnectors({ signal: abortCtrl.signal }); - if (!didCancel) { - setLoading(false); - setConnectors(res); - } - } catch (error) { - if (!didCancel) { - setLoading(false); - setConnectors([]); + const refetchConnectors = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + setLoading(true); + const res = await fetchConnectors({ signal: abortCtrlRef.current.signal }); + + if (!isCancelledRef.current) { + setLoading(false); + setConnectors(res); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); } + + setLoading(false); + setConnectors([]); } - }; - getConnectors(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { refetchConnectors(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx index 0fe45aaab799b..da069ee6f1075 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useReducer } from 'react'; +import { useCallback, useReducer, useRef, useEffect } from 'react'; import { CaseStatuses } from '../../../../case/common/api'; import { displaySuccessToast, @@ -87,49 +87,45 @@ export const useUpdateCases = (): UseUpdateCases => { isUpdated: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[], action: string) => { - let cancel = false; - const abortCtrl = new AbortController(); + const dispatchUpdateCases = useCallback(async (cases: BulkUpdateStatus[], action: string) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); - const patchData = async () => { - try { - dispatch({ type: 'FETCH_INIT' }); - const patchResponse = await patchCasesStatus(cases, abortCtrl.signal); - if (!cancel) { - const resultCount = Object.keys(patchResponse).length; - const firstTitle = patchResponse[0].title; + dispatch({ type: 'FETCH_INIT' }); + const patchResponse = await patchCasesStatus(cases, abortCtrlRef.current.signal); - dispatch({ type: 'FETCH_SUCCESS', payload: true }); + if (!isCancelledRef.current) { + const resultCount = Object.keys(patchResponse).length; + const firstTitle = patchResponse[0].title; - const messageArgs = { - totalCases: resultCount, - caseTitle: resultCount === 1 ? firstTitle : '', - }; + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { + totalCases: resultCount, + caseTitle: resultCount === 1 ? firstTitle : '', + }; - const message = - action === 'status' - ? getStatusToasterMessage(patchResponse[0].status, messageArgs) - : ''; + const message = + action === 'status' ? getStatusToasterMessage(patchResponse[0].status, messageArgs) : ''; - displaySuccessToast(message, dispatchToaster); - } - } catch (error) { - if (!cancel) { + displaySuccessToast(message, dispatchToaster); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - patchData(); - return () => { - cancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -137,14 +133,25 @@ export const useUpdateCases = (): UseUpdateCases => { dispatch({ type: 'RESET_IS_UPDATED' }); }, []); - const updateBulkStatus = useCallback((cases: Case[], status: string) => { - const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({ - status, - id: theCase.id, - version: theCase.version, - })); - dispatchUpdateCases(updateCasesStatus, 'status'); + const updateBulkStatus = useCallback( + (cases: Case[], status: string) => { + const updateCasesStatus: BulkUpdateStatus[] = cases.map((theCase) => ({ + status, + id: theCase.id, + version: theCase.version, + })); + dispatchUpdateCases(updateCasesStatus, 'status'); + }, // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; }, []); + return { ...state, updateBulkStatus, dispatchResetIsUpdated }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx index 923c20dcf8ebd..f3d59a2883f2a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useReducer } from 'react'; +import { useCallback, useReducer, useRef, useEffect } from 'react'; import { displaySuccessToast, errorToToaster, @@ -78,45 +78,43 @@ export const useDeleteCases = (): UseDeleteCase => { isDeleted: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => { - let cancel = false; - const abortCtrl = new AbortController(); + const dispatchDeleteCases = useCallback(async (cases: DeleteCase[]) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT' }); - const deleteData = async () => { - try { - dispatch({ type: 'FETCH_INIT' }); - const caseIds = cases.map((theCase) => theCase.id); - // We don't allow user batch delete sub cases on UI at the moment. - if (cases[0].type != null || cases.length > 1) { - await deleteCases(caseIds, abortCtrl.signal); - } else { - await deleteSubCases(caseIds, abortCtrl.signal); - } + const caseIds = cases.map((theCase) => theCase.id); + // We don't allow user batch delete sub cases on UI at the moment. + if (cases[0].type != null || cases.length > 1) { + await deleteCases(caseIds, abortCtrlRef.current.signal); + } else { + await deleteSubCases(caseIds, abortCtrlRef.current.signal); + } - if (!cancel) { - dispatch({ type: 'FETCH_SUCCESS', payload: true }); - displaySuccessToast( - i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), - dispatchToaster - ); - } - } catch (error) { - if (!cancel) { + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + displaySuccessToast( + i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), + dispatchToaster + ); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_DELETING, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - deleteData(); - return () => { - abortCtrl.abort(); - cancel = true; - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -142,5 +140,12 @@ export const useDeleteCases = (): UseDeleteCase => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.isDisplayConfirmDeleteModal]); + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx index 9b536f32e7eb8..9b10247794c8d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_action_license.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getActionLicense } from './api'; @@ -28,53 +28,58 @@ const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira'; export const useGetActionLicense = (): ActionLicenseState => { const [actionLicenseState, setActionLicensesState] = useState(initialData); - const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const fetchActionLicense = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchActionLicense = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); setActionLicensesState({ - ...actionLicenseState, + ...initialData, isLoading: true, }); - try { - const response = await getActionLicense(abortCtrl.signal); - if (!didCancel) { - setActionLicensesState({ - actionLicense: - response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { + + const response = await getActionLicense(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + setActionLicensesState({ + actionLicense: response.find((l) => l.id === MINIMUM_LICENSE_REQUIRED_CONNECTOR) ?? null, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setActionLicensesState({ - actionLicense: null, - isLoading: false, - isError: true, - }); } + + setActionLicensesState({ + actionLicense: null, + isLoading: false, + isError: true, + }); } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionLicenseState]); useEffect(() => { fetchActionLicense(); + + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return { ...actionLicenseState }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx index a157be2dc1353..a3d64a17727e5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.test.tsx @@ -6,7 +6,7 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { initialData, useGetCase, UseGetCase } from './use_get_case'; +import { useGetCase, UseGetCase } from './use_get_case'; import { basicCase } from './mock'; import * as api from './api'; @@ -26,8 +26,8 @@ describe('useGetCase', () => { ); await waitForNextUpdate(); expect(result.current).toEqual({ - data: initialData, - isLoading: true, + data: null, + isLoading: false, isError: false, fetchCase: result.current.fetchCase, updateCase: result.current.updateCase, @@ -102,7 +102,7 @@ describe('useGetCase', () => { await waitForNextUpdate(); expect(result.current).toEqual({ - data: initialData, + data: null, isLoading: false, isError: true, fetchCase: result.current.fetchCase, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx index 1c4476e3cb2b7..70e202b5d6bdf 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx @@ -5,18 +5,15 @@ * 2.0. */ -import { isEmpty } from 'lodash'; import { useEffect, useReducer, useCallback, useRef } from 'react'; -import { CaseStatuses, CaseType } from '../../../../case/common/api'; import { Case } from './types'; import * as i18n from './translations'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCase, getSubCase } from './api'; -import { getNoneConnector } from '../components/configure_cases/utils'; interface CaseState { - data: Case; + data: Case | null; isLoading: boolean; isError: boolean; } @@ -57,32 +54,6 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { return state; } }; -export const initialData: Case = { - id: '', - closedAt: null, - closedBy: null, - createdAt: '', - comments: [], - connector: { ...getNoneConnector(), fields: null }, - createdBy: { - username: '', - }, - description: '', - externalService: null, - status: CaseStatuses.open, - tags: [], - title: '', - totalAlerts: 0, - totalComment: 0, - type: CaseType.individual, - updatedAt: null, - updatedBy: null, - version: '', - subCaseIds: [], - settings: { - syncAlerts: true, - }, -}; export interface UseGetCase extends CaseState { fetchCase: () => void; @@ -91,53 +62,53 @@ export interface UseGetCase extends CaseState { export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { const [state, dispatch] = useReducer(dataFetchReducer, { - isLoading: true, + isLoading: false, isError: false, - data: initialData, + data: null, }); const [, dispatchToaster] = useStateToaster(); - const abortCtrl = useRef(new AbortController()); - const didCancel = useRef(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const updateCase = useCallback((newCase: Case) => { dispatch({ type: 'UPDATE_CASE', payload: newCase }); }, []); const callFetch = useCallback(async () => { - const fetchData = async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - try { - const response = await (subCaseId - ? getSubCase(caseId, subCaseId, true, abortCtrl.current.signal) - : getCase(caseId, true, abortCtrl.current.signal)); - if (!didCancel.current) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!didCancel.current) { + + const response = await (subCaseId + ? getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal) + : getCase(caseId, true, abortCtrlRef.current.signal)); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - didCancel.current = false; - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - fetchData(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseId, subCaseId]); useEffect(() => { - if (!isEmpty(caseId)) { - callFetch(); - } + callFetch(); + return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseId, subCaseId]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx index 12e5f6643351f..cc8deaf72eef6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx @@ -6,7 +6,7 @@ */ import { isEmpty, uniqBy } from 'lodash/fp'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; @@ -244,64 +244,67 @@ export const useGetCaseUserActions = ( const [caseUserActionsState, setCaseUserActionsState] = useState( initialData ); - const abortCtrl = useRef(new AbortController()); - const didCancel = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + const isCancelledRef = useRef(false); const [, dispatchToaster] = useStateToaster(); const fetchCaseUserActions = useCallback( - (thisCaseId: string, thisSubCaseId?: string) => { - const fetchData = async () => { - try { + async (thisCaseId: string, thisSubCaseId?: string) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setCaseUserActionsState({ + ...caseUserActionsState, + isLoading: true, + }); + + const response = await (thisSubCaseId + ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrlRef.current.signal) + : getCaseUserActions(thisCaseId, abortCtrlRef.current.signal)); + + if (!isCancelledRef.current) { + // Attention Future developer + // We are removing the first item because it will always be the creation of the case + // and we do not want it to simplify our life + const participants = !isEmpty(response) + ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy) + : []; + + const caseUserActions = !isEmpty(response) + ? thisSubCaseId + ? response + : response.slice(1) + : []; + setCaseUserActionsState({ - ...caseUserActionsState, - isLoading: true, + caseUserActions, + ...getPushedInfo(caseUserActions, caseConnectorId), + isLoading: false, + isError: false, + participants, }); - - const response = await (thisSubCaseId - ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrl.current.signal) - : getCaseUserActions(thisCaseId, abortCtrl.current.signal)); - if (!didCancel.current) { - // Attention Future developer - // We are removing the first item because it will always be the creation of the case - // and we do not want it to simplify our life - const participants = !isEmpty(response) - ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy) - : []; - - const caseUserActions = !isEmpty(response) - ? thisSubCaseId - ? response - : response.slice(1) - : []; - setCaseUserActionsState({ - caseUserActions, - ...getPushedInfo(caseUserActions, caseConnectorId), - isLoading: false, - isError: false, - participants, - }); - } - } catch (error) { - if (!didCancel.current) { + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setCaseUserActionsState({ - caseServices: {}, - caseUserActions: [], - hasDataToPush: false, - isError: true, - isLoading: false, - participants: [], - }); } + + setCaseUserActionsState({ + caseServices: {}, + caseUserActions: [], + hasDataToPush: false, + isError: true, + isLoading: false, + participants: [], + }); } - }; - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - fetchData(); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [caseConnectorId] @@ -313,8 +316,8 @@ export const useGetCaseUserActions = ( } return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [caseId, subCaseId]); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index 298d817fffa88..c83cc02dedb97 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useReducer } from 'react'; +import { useCallback, useEffect, useReducer, useRef } from 'react'; import { CaseStatuses } from '../../../../case/common/api'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types'; @@ -139,6 +139,10 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { selectedCases: [], }); const [, dispatchToaster] = useStateToaster(); + const didCancelFetchCases = useRef(false); + const didCancelUpdateCases = useRef(false); + const abortCtrlFetchCases = useRef(new AbortController()); + const abortCtrlUpdateCases = useRef(new AbortController()); const setSelectedCases = useCallback((mySelectedCases: Case[]) => { dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: mySelectedCases }); @@ -152,81 +156,69 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { dispatch({ type: 'UPDATE_FILTER_OPTIONS', payload: newFilters }); }, []); - const fetchCases = useCallback((filterOptions: FilterOptions, queryParams: QueryParams) => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { + const fetchCases = useCallback(async (filterOptions: FilterOptions, queryParams: QueryParams) => { + try { + didCancelFetchCases.current = false; + abortCtrlFetchCases.current.abort(); + abortCtrlFetchCases.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: 'cases' }); - try { - const response = await getCases({ - filterOptions, - queryParams, - signal: abortCtrl.signal, + + const response = await getCases({ + filterOptions, + queryParams, + signal: abortCtrlFetchCases.current.signal, + }); + + if (!didCancelFetchCases.current) { + dispatch({ + type: 'FETCH_CASES_SUCCESS', + payload: response, }); - if (!didCancel) { - dispatch({ - type: 'FETCH_CASES_SUCCESS', - payload: response, - }); - } - } catch (error) { - if (!didCancel) { + } + } catch (error) { + if (!didCancelFetchCases.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); } + dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); } - }; - fetchData(); - return () => { - abortCtrl.abort(); - didCancel = true; - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => fetchCases(state.filterOptions, state.queryParams), [ - state.queryParams, - state.filterOptions, - ]); - const dispatchUpdateCaseProperty = useCallback( - ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { + async ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => { + try { + didCancelUpdateCases.current = false; + abortCtrlUpdateCases.current.abort(); + abortCtrlUpdateCases.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); - try { - await patchCase( - caseId, - { [updateKey]: updateValue }, - // saved object versions are typed as string | undefined, hope that's not true - version ?? '', - abortCtrl.signal - ); - if (!didCancel) { - dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); - fetchCases(state.filterOptions, state.queryParams); - refetchCasesStatus(); - } - } catch (error) { - if (!didCancel) { + + await patchCase( + caseId, + { [updateKey]: updateValue }, + // saved object versions are typed as string | undefined, hope that's not true + version ?? '', + abortCtrlUpdateCases.current.signal + ); + + if (!didCancelUpdateCases.current) { + dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); + fetchCases(state.filterOptions, state.queryParams); + refetchCasesStatus(); + } + } catch (error) { + if (!didCancelUpdateCases.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); - dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); } + dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); } - }; - fetchData(); - return () => { - abortCtrl.abort(); - didCancel = true; - }; + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [state.filterOptions, state.queryParams] @@ -237,6 +229,17 @@ export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.filterOptions, state.queryParams]); + useEffect(() => { + fetchCases(state.filterOptions, state.queryParams); + return () => { + didCancelFetchCases.current = true; + didCancelUpdateCases.current = true; + abortCtrlFetchCases.current.abort(); + abortCtrlUpdateCases.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.queryParams, state.filterOptions]); + return { ...state, dispatchUpdateCaseProperty, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx index 057fc05008bb0..087f7ef455cba 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCasesStatus } from './api'; @@ -32,51 +32,56 @@ export interface UseGetCasesStatus extends CasesStatusState { export const useGetCasesStatus = (): UseGetCasesStatus => { const [casesStatusState, setCasesStatusState] = useState(initialData); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const fetchCasesStatus = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchCasesStatus = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); setCasesStatusState({ - ...casesStatusState, + ...initialData, isLoading: true, }); - try { - const response = await getCasesStatus(abortCtrl.signal); - if (!didCancel) { - setCasesStatusState({ - ...response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { + + const response = await getCasesStatus(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + setCasesStatusState({ + ...response, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setCasesStatusState({ - countClosedCases: 0, - countInProgressCases: 0, - countOpenCases: 0, - isLoading: false, - isError: true, - }); } + setCasesStatusState({ + countClosedCases: 0, + countInProgressCases: 0, + countOpenCases: 0, + isLoading: false, + isError: true, + }); } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [casesStatusState]); + }, []); useEffect(() => { fetchCasesStatus(); + + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx index 25c483045b84f..f2c33ec4730fe 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_reporters.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; - +import { useCallback, useEffect, useState, useRef } from 'react'; import { isEmpty } from 'lodash/fp'; + import { User } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getReporters } from './api'; @@ -35,57 +35,61 @@ export const useGetReporters = (): UseGetReporters => { const [reportersState, setReporterState] = useState(initialData); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const fetchReporters = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { + const fetchReporters = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); setReporterState({ ...reportersState, isLoading: true, }); - try { - const response = await getReporters(abortCtrl.signal); - const myReporters = response - .map((r) => - r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name - ) - .filter((u) => !isEmpty(u)); - if (!didCancel) { - setReporterState({ - reporters: myReporters, - respReporters: response, - isLoading: false, - isError: false, - }); - } - } catch (error) { - if (!didCancel) { + + const response = await getReporters(abortCtrlRef.current.signal); + const myReporters = response + .map((r) => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name)) + .filter((u) => !isEmpty(u)); + + if (!isCancelledRef.current) { + setReporterState({ + reporters: myReporters, + respReporters: response, + isLoading: false, + isError: false, + }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - setReporterState({ - reporters: [], - respReporters: [], - isLoading: false, - isError: true, - }); } + + setReporterState({ + reporters: [], + respReporters: [], + isLoading: false, + isError: true, + }); } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportersState]); useEffect(() => { fetchReporters(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + return { ...reportersState, fetchReporters }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx index 208516d302eb4..4a7a298e2cd86 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_tags.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { useEffect, useReducer } from 'react'; - +import { useEffect, useReducer, useRef, useCallback } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getTags } from './api'; import * as i18n from './translations'; @@ -59,37 +58,42 @@ export const useGetTags = (): UseGetTags => { tags: initialData, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); - const callFetch = () => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { + const callFetch = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - try { - const response = await getTags(abortCtrl.signal); - if (!didCancel) { - dispatch({ type: 'FETCH_SUCCESS', payload: response }); - } - } catch (error) { - if (!didCancel) { + + const response = await getTags(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }; - fetchData(); - return () => { - abortCtrl.abort(); - didCancel = true; - }; - }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { callFetch(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { ...state, fetchTags: callFetch }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx index c4fa030473534..d890c050f5034 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx @@ -6,7 +6,6 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; - import { CasePostRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postCase } from './api'; @@ -51,38 +50,41 @@ export const usePostCase = (): UsePostCase => { isError: false, }); const [, dispatchToaster] = useStateToaster(); - const cancel = useRef(false); - const abortCtrl = useRef(new AbortController()); - const postMyCase = useCallback( - async (data: CasePostRequest) => { - try { - dispatch({ type: 'FETCH_INIT' }); - abortCtrl.current.abort(); - cancel.current = false; - abortCtrl.current = new AbortController(); - const response = await postCase(data, abortCtrl.current.signal); - if (!cancel.current) { - dispatch({ type: 'FETCH_SUCCESS' }); - } - return response; - } catch (error) { - if (!cancel.current) { + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const postMyCase = useCallback(async (data: CasePostRequest) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + dispatch({ type: 'FETCH_INIT' }); + const response = await postCase(data, abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS' }); + } + return response; + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: 'FETCH_FAILURE' }); } + dispatch({ type: 'FETCH_FAILURE' }); } - }, - [dispatchToaster] - ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { return () => { - abortCtrl.current.abort(); - cancel.current = true; + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; }, []); return { ...state, postCase: postMyCase }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index 8fc8053c14f70..75d3047bc828e 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; - +import { useReducer, useCallback, useRef, useEffect } from 'react'; import { CommentRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; @@ -49,7 +48,7 @@ interface PostComment { subCaseId?: string; } export interface UsePostComment extends NewCommentState { - postComment: (args: PostComment) => void; + postComment: (args: PostComment) => Promise; } export const usePostComment = (): UsePostComment => { @@ -58,38 +57,47 @@ export const usePostComment = (): UsePostComment => { isError: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const postMyComment = useCallback( async ({ caseId, data, updateCase, subCaseId }: PostComment) => { - let cancel = false; - const abortCtrl = new AbortController(); - try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT' }); - const response = await postComment(data, caseId, abortCtrl.signal, subCaseId); - if (!cancel) { + + const response = await postComment(data, caseId, abortCtrlRef.current.signal, subCaseId); + + if (!isCancelledRef.current) { dispatch({ type: 'FETCH_SUCCESS' }); if (updateCase) { updateCase(response); } } } catch (error) { - if (!cancel) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE' }); } } - return () => { - abortCtrl.abort(); - cancel = true; - }; }, [dispatchToaster] ); + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, postComment: postMyComment }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index 03d881d7934e9..27a02d9300cc0 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -67,17 +67,17 @@ export const usePostPushToService = (): UsePostPushToService => { }); const [, dispatchToaster] = useStateToaster(); const cancel = useRef(false); - const abortCtrl = useRef(new AbortController()); + const abortCtrlRef = useRef(new AbortController()); const pushCaseToExternalService = useCallback( async ({ caseId, connector }: PushToServiceRequest) => { try { - dispatch({ type: 'FETCH_INIT' }); - abortCtrl.current.abort(); + abortCtrlRef.current.abort(); cancel.current = false; - abortCtrl.current = new AbortController(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT' }); - const response = await pushCase(caseId, connector.id, abortCtrl.current.signal); + const response = await pushCase(caseId, connector.id, abortCtrlRef.current.signal); if (!cancel.current) { dispatch({ type: 'FETCH_SUCCESS' }); @@ -90,11 +90,13 @@ export const usePostPushToService = (): UsePostPushToService => { return response; } catch (error) { if (!cancel.current) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE' }); } } @@ -105,7 +107,7 @@ export const usePostPushToService = (): UsePostPushToService => { useEffect(() => { return () => { - abortCtrl.current.abort(); + abortCtrlRef.current.abort(); cancel.current = true; }; }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index 23a23caeb71bd..e8de2257009e6 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { useReducer, useCallback, useEffect, useRef } from 'react'; +import { useReducer, useCallback, useRef, useEffect } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; - import { patchCase, patchSubCase } from './api'; import { UpdateKey, UpdateByKey, CaseStatuses } from './types'; import * as i18n from './translations'; @@ -70,8 +69,8 @@ export const useUpdateCase = ({ updateKey: null, }); const [, dispatchToaster] = useStateToaster(); - const abortCtrl = useRef(new AbortController()); - const didCancel = useRef(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const dispatchUpdateCaseProperty = useCallback( async ({ @@ -84,24 +83,27 @@ export const useUpdateCase = ({ onError, }: UpdateByKey) => { try { - didCancel.current = false; - abortCtrl.current = new AbortController(); + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: updateKey }); + const response = await (updateKey === 'status' && subCaseId ? patchSubCase( caseId, subCaseId, { status: updateValue as CaseStatuses }, caseData.version, - abortCtrl.current.signal + abortCtrlRef.current.signal ) : patchCase( caseId, { [updateKey]: updateValue }, caseData.version, - abortCtrl.current.signal + abortCtrlRef.current.signal )); - if (!didCancel.current) { + + if (!isCancelledRef.current) { if (fetchCaseUserActions != null) { fetchCaseUserActions(caseId, subCaseId); } @@ -119,7 +121,7 @@ export const useUpdateCase = ({ } } } catch (error) { - if (!didCancel.current) { + if (!isCancelledRef.current) { if (error.name !== 'AbortError') { errorToToaster({ title: i18n.ERROR_TITLE, @@ -140,8 +142,8 @@ export const useUpdateCase = ({ useEffect(() => { return () => { - didCancel.current = true; - abortCtrl.current.abort(); + isCancelledRef.current = true; + abortCtrlRef.current.abort(); }; }, []); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx index e36b21823310e..81bce248852fe 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; - +import { useReducer, useCallback, useRef, useEffect } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; - import { patchComment } from './api'; import * as i18n from './translations'; import { Case } from './types'; @@ -72,6 +70,8 @@ export const useUpdateComment = (): UseUpdateComment => { isError: false, }); const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); const dispatchUpdateComment = useCallback( async ({ @@ -83,41 +83,49 @@ export const useUpdateComment = (): UseUpdateComment => { updateCase, version, }: UpdateComment) => { - let cancel = false; - const abortCtrl = new AbortController(); try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: commentId }); + const response = await patchComment( caseId, commentId, commentUpdate, version, - abortCtrl.signal, + abortCtrlRef.current.signal, subCaseId ); - if (!cancel) { + + if (!isCancelledRef.current) { updateCase(response); fetchUserActions(); dispatch({ type: 'FETCH_SUCCESS', payload: { commentId } }); } } catch (error) { - if (!cancel) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE', payload: commentId }); } } - return () => { - cancel = true; - abortCtrl.abort(); - }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [] ); + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, patchComment: dispatchUpdateComment }; }; diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index caaa1f6e248ea..b7cfe11aafda0 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -294,3 +294,10 @@ export const ALERT_ADDED_TO_CASE = i18n.translate( defaultMessage: 'added to case', } ); + +export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate( + 'xpack.securitySolution.common.allCases.table.selectableMessageCollections', + { + defaultMessage: 'Cases with sub-cases cannot be selected', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx index 11f424d83c530..a6dd64737f5ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/index.test.tsx @@ -8,14 +8,15 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { AndOrBadge } from './'; +const mockTheme = { eui: { euiColorLightShade: '#ece' } }; + describe('AndOrBadge', () => { test('it renders top and bottom antenna bars when "includeAntennas" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -27,7 +28,7 @@ describe('AndOrBadge', () => { test('it does not render top and bottom antenna bars when "includeAntennas" is false', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -39,7 +40,7 @@ describe('AndOrBadge', () => { test('it renders "and" when "type" is "and"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -49,7 +50,7 @@ describe('AndOrBadge', () => { test('it renders "or" when "type" is "or"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx index 5bc89baa3d415..489d02990b1f4 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge.test.tsx @@ -6,29 +6,19 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { RoundedBadge } from './rounded_badge'; describe('RoundedBadge', () => { test('it renders "and" when "type" is "and"', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('AND'); }); test('it renders "or" when "type" is "or"', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="and-or-badge"]').at(0).text()).toEqual('OR'); }); diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx index 61cf44293005f..c6536a05be45d 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.test.tsx @@ -8,14 +8,15 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { RoundedBadgeAntenna } from './rounded_badge_antenna'; +const mockTheme = { eui: { euiColorLightShade: '#ece' } }; + describe('RoundedBadgeAntenna', () => { test('it renders top and bottom antenna bars', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -27,7 +28,7 @@ describe('RoundedBadgeAntenna', () => { test('it renders "and" when "type" is "and"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -37,7 +38,7 @@ describe('RoundedBadgeAntenna', () => { test('it renders "or" when "type" is "or"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx index 6a3d82d47045a..79e6fe5506b84 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { @@ -20,21 +18,19 @@ import { FieldComponent } from './field'; describe('FieldComponent', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -44,21 +40,19 @@ describe('FieldComponent', () => { test('it renders loading if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); expect( @@ -70,21 +64,19 @@ describe('FieldComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -96,21 +88,19 @@ describe('FieldComponent', () => { test('it correctly displays selected field', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -121,21 +111,19 @@ describe('FieldComponent', () => { test('it invokes "onChange" when option selected', () => { const mockOnChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx index f577799827b89..b6300581f12dd 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx @@ -6,19 +6,13 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { AutocompleteFieldExistsComponent } from './field_value_exists'; describe('AutocompleteFieldExistsComponent', () => { test('it renders field disabled', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect( wrapper diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index e01cc5ff1e042..c605a71c50e33 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { waitFor } from '@testing-library/react'; @@ -47,17 +45,15 @@ jest.mock('../../../lists_plugin_deps', () => { describe('AutocompleteFieldListsComponent', () => { test('it renders disabled if "isDisabled" is true', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -69,17 +65,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it renders loading if "isLoading" is true', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper @@ -97,17 +91,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it allows user to clear values if "isClearable" is true', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( wrapper @@ -118,17 +110,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it correctly displays lists that match the selected "keyword" field esType', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); @@ -142,17 +132,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it correctly displays lists that match the selected "ip" field esType', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); @@ -166,17 +154,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it correctly displays selected list', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -190,17 +176,15 @@ describe('AutocompleteFieldListsComponent', () => { test('it invokes "onChange" when option selected', async () => { const mockOnChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx index d4712092867e9..38d103fe65130 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiSuperSelect, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; @@ -44,23 +42,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it renders row label if one passed in', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -70,23 +66,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it renders disabled if "isDisabled" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -96,23 +90,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it renders loading if "isLoading" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="valuesAutocompleteMatch"] button').at(0).simulate('click'); expect( @@ -124,23 +116,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -152,23 +142,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it correctly displays selected value', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -179,23 +167,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" when new value created', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -208,23 +194,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" when new value selected', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -236,23 +220,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it refreshes autocomplete with search query when new value searched', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); act(() => { ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -287,23 +269,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it displays only two options - "true" or "false"', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -326,23 +306,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" with "true" when selected', () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiSuperSelect).props() as unknown) as { @@ -355,23 +333,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" with "false" when selected', () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiSuperSelect).props() as unknown) as { @@ -396,23 +372,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it number input when field type is number', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -423,23 +397,21 @@ describe('AutocompleteFieldMatchComponent', () => { test('it invokes "onChange" with numeric value when inputted', () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx index aa2038262f40c..6b479c5ab8c4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { act } from '@testing-library/react'; @@ -43,24 +41,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it renders disabled if "isDisabled" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -70,24 +66,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it renders loading if "isLoading" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find(`[data-test-subj="valuesAutocompleteMatchAny"] button`).at(0).simulate('click'); expect( @@ -99,24 +93,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -128,24 +120,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it correctly displays selected value', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -156,24 +146,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it invokes "onChange" when new value created', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -186,24 +174,22 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it invokes "onChange" when new value selected', async () => { const mockOnChange = jest.fn(); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { @@ -215,23 +201,21 @@ describe('AutocompleteFieldMatchAnyComponent', () => { test('it refreshes autocomplete with search query when new value searched', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); act(() => { ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx index 56ae6d762e7ee..db16cbde2acb4 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -18,17 +16,15 @@ import { isOperator, isNotOperator } from './operators'; describe('OperatorComponent', () => { test('it renders disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -38,17 +34,15 @@ describe('OperatorComponent', () => { test('it renders loading if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] button`).at(0).simulate('click'); expect( @@ -60,17 +54,15 @@ describe('OperatorComponent', () => { test('it allows user to clear values if "isClearable" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find(`button[data-test-subj="comboBoxClearButton"]`).exists()).toBeTruthy(); @@ -78,18 +70,16 @@ describe('OperatorComponent', () => { test('it displays "operatorOptions" if param is passed in with items', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -99,18 +89,16 @@ describe('OperatorComponent', () => { test('it does not display "operatorOptions" if param is passed in with no items', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -145,17 +133,15 @@ describe('OperatorComponent', () => { test('it correctly displays selected operator', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -165,27 +151,25 @@ describe('OperatorComponent', () => { test('it only displays subset of operators if field type is nested', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -195,17 +179,15 @@ describe('OperatorComponent', () => { test('it only displays subset of operators if field type is boolean', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -221,17 +203,15 @@ describe('OperatorComponent', () => { test('it invokes "onChange" when option selected', () => { const mockOnChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); ((wrapper.find(EuiComboBox).props() as unknown) as { diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 096bea37566b3..6d87b5d3a68b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -6,10 +6,8 @@ */ import { Chart, BarSeries, Axis, ScaleType } from '@elastic/charts'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { TestProviders } from '../../mock'; @@ -37,8 +35,6 @@ jest.mock('uuid', () => { }; }); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const customHeight = '100px'; const customWidth = '120px'; const chartDataSets = [ @@ -323,11 +319,9 @@ describe.each(chartDataSets)('BarChart with stackByField', () => { beforeAll(() => { wrapper = mount( - - - - - + + + ); }); @@ -407,11 +401,9 @@ describe.each(chartDataSets)('BarChart with custom color', () => { beforeAll(() => { wrapper = mount( - - - - - + + + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx index fb91a4a3ce92b..544f9b1abf8f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; @@ -26,8 +24,6 @@ jest.mock('@elastic/eui', () => { }; }); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const allOthersDataProviderId = 'draggable-legend-item-527adabe-8e1c-4a1f-965c-2f3d65dda9e1-event_dataset-All others'; @@ -74,11 +70,9 @@ describe('DraggableLegend', () => { beforeEach(() => { wrapper = mount( - - - - - + + + ); }); @@ -120,11 +114,9 @@ describe('DraggableLegend', () => { it('does NOT render the legend when an empty collection of legendItems is provided', () => { const wrapper = mount( - - - - - + + + ); expect(wrapper.find('[data-test-subj="draggable-legend"]').exists()).toBe(false); @@ -132,11 +124,9 @@ describe('DraggableLegend', () => { it(`renders a legend with the minimum height when 'height' is zero`, () => { const wrapper = mount( - - - - - + + + ); expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule( diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx index 15c164e59557d..4958f6bae4a30 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; @@ -25,8 +23,6 @@ jest.mock('@elastic/eui', () => { }; }); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - describe('DraggableLegendItem', () => { describe('rendering a regular (non "All others") legend item', () => { const legendItem: LegendItem = { @@ -41,11 +37,9 @@ describe('DraggableLegendItem', () => { beforeEach(() => { wrapper = mount( - - - - - + + + ); }); @@ -79,11 +73,9 @@ describe('DraggableLegendItem', () => { beforeEach(() => { wrapper = mount( - - - - - + + + ); }); @@ -118,11 +110,9 @@ describe('DraggableLegendItem', () => { }; const wrapper = mount( - - - - - + + + ); expect(wrapper.find('[data-test-subj="legend-color"]').exists()).toBe(false); diff --git a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx index 764d9109816b5..e3c74bf425628 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/empty_value/empty_value.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -21,7 +20,7 @@ import { } from '.'; describe('EmptyValue', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; test('it renders against snapshot', () => { const wrapper = shallow(

{getEmptyString()}

); @@ -35,7 +34,7 @@ describe('EmptyValue', () => { describe('#getEmptyString', () => { test('should turn into an empty string place holder', () => { const wrapper = mountWithIntl( - +

{getEmptyString()}

); @@ -45,7 +44,7 @@ describe('EmptyValue', () => { describe('#getEmptyTagValue', () => { const wrapper = mount( - +

{getEmptyTagValue()}

); @@ -55,7 +54,7 @@ describe('EmptyValue', () => { describe('#getEmptyStringTag', () => { test('should turn into an span that has length of 1', () => { const wrapper = mountWithIntl( - +

{getEmptyStringTag()}

); @@ -64,7 +63,7 @@ describe('EmptyValue', () => { test('should turn into an empty string tag place holder', () => { const wrapper = mountWithIntl( - +

{getEmptyStringTag()}

); @@ -75,7 +74,7 @@ describe('EmptyValue', () => { describe('#defaultToEmptyTag', () => { test('should default to an empty value when a value is null', () => { const wrapper = mount( - +

{defaultToEmptyTag(null)}

); @@ -84,7 +83,7 @@ describe('EmptyValue', () => { test('should default to an empty value when a value is undefined', () => { const wrapper = mount( - +

{defaultToEmptyTag(undefined)}

); @@ -114,7 +113,7 @@ describe('EmptyValue', () => { }, }; const wrapper = mount( - +

{getOrEmptyTag('a.b.c', test)}

); @@ -130,7 +129,7 @@ describe('EmptyValue', () => { }, }; const wrapper = mount( - +

{getOrEmptyTag('a.b.c', test)}

); @@ -144,7 +143,7 @@ describe('EmptyValue', () => { }, }; const wrapper = mount( - +

{getOrEmptyTag('a.b.c', test)}

); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index af76a79f0e330..9ba6fe104be45 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount, ReactWrapper } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { waitFor } from '@testing-library/react'; import { AddExceptionModal } from './'; @@ -32,6 +31,17 @@ import { import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { AlertData } from '../types'; +const mockTheme = { + eui: { + euiBreakpoints: { + l: '1200px', + }, + paddingSizes: { + m: '10px', + }, + }, +}; + jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../common/lib/kibana'); jest.mock('../../../containers/source'); @@ -101,7 +111,7 @@ describe('When the add exception modal is opened', () => { }, ]); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { file: { path: 'test/path' }, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('when there are exception builder errors submit button is disabled', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -27,7 +28,7 @@ describe('BuilderAndBadgeComponent', () => { test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -39,7 +40,7 @@ describe('BuilderAndBadgeComponent', () => { test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx index 6505c5eb2b310..1ea54473032cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { useKibana } from '../../../../common/lib/kibana'; import { fields } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -18,6 +17,12 @@ import { getEntryMatchAnyMock } from '../../../../../../lists/common/schemas/typ import { BuilderExceptionListItemComponent } from './exception_item'; +const mockTheme = { + eui: { + euiColorLightShade: '#ece', + }, +}; + jest.mock('../../../../common/lib/kibana'); describe('BuilderExceptionListItemComponent', () => { @@ -46,7 +51,7 @@ describe('BuilderExceptionListItemComponent', () => { entries: [getEntryMatchMock(), getEntryMatchMock()], }; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock(), getEntryMatchMock()]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [getEntryMatchMock()]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { @@ -50,7 +55,7 @@ describe('ExceptionBuilderComponent', () => { test('it displays empty entry if no "exceptionListItems" are passed in', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "exceptionListItems" that are passed in', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "or", "and" and "add nested button" enabled', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an entry when "and" clicked', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an exception item when "or" clicked', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays empty entry if user deletes last remaining entry', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "and" badge if at least one exception item includes more than one entry', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it does not display "and" badge if none of the exception items include more than one entry', () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { describe('nested entry', () => { test('it adds a nested entry when "add nested entry" clicked', async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { }, ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { ] as EntriesArray, }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { }, })); wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { let wrapper: ReactWrapper; beforeEach(async () => { wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { beforeEach(async () => { const exceptionItemMock = { ...getExceptionListItemSchemaMock(), entries: [] }; wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('when there are exception builder errors has the add exception button disabled', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders error details', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -57,21 +53,19 @@ describe('ErrorCallout', () => { it('it invokes "onCancel" when cancel button clicked', () => { const mockOnCancel = jest.fn(); const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="errorCalloutCancelButton"]').at(0).simulate('click'); @@ -81,21 +75,19 @@ describe('ErrorCallout', () => { it('it does not render status code if not available', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -109,21 +101,19 @@ describe('ErrorCallout', () => { it('it renders specific missing exceptions list error', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -137,21 +127,19 @@ describe('ErrorCallout', () => { it('it dissasociates list from rule when remove exception list clicked ', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="errorCalloutDissasociateButton"]').at(0).simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index c7d7d2d39393c..b96ae5c06dd22 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -8,13 +8,18 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import moment from 'moment-timezone'; import { ExceptionDetails } from './exception_details'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comment.mock'; +const mockTheme = { + eui: { + euiColorLightestShade: '#ece', + }, +}; + describe('ExceptionDetails', () => { beforeEach(() => { moment.tz.setDefault('UTC'); @@ -29,7 +34,7 @@ describe('ExceptionDetails', () => { exceptionItem.comments = []; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = [getCommentsArrayMock()[0]]; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the operating system if one is specified in the exception item', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the exception item creator', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the exception item creation timestamp', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the description if one is included on the exception item', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it does NOT render the and badge if only one exception item entry exists', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders the and badge if more than one exception item exists', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it invokes "onEdit" when edit button clicked', () => { const mockOnEdit = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it invokes "onDelete" when delete button clicked', () => { const mockOnDelete = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders edit button disabled if "disableDelete" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders delete button in loading state if "disableDelete" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { parentEntry.value = undefined; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders non-nested entries', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders ExceptionDetails and ExceptionEntries', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders passed in "pageSize" as selected option', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsPerPageBtn"]').at(0).text()).toEqual( @@ -35,17 +31,15 @@ describe('ExceptionsViewerPagination', () => { it('it renders all passed in page size options when per page button clicked', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); @@ -64,17 +58,15 @@ describe('ExceptionsViewerPagination', () => { it('it invokes "onPaginationChange" when per page item is clicked', () => { const mockOnPaginationChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); @@ -87,17 +79,15 @@ describe('ExceptionsViewerPagination', () => { it('it renders correct total page count', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('pageCount')).toEqual( @@ -111,17 +101,15 @@ describe('ExceptionsViewerPagination', () => { it('it invokes "onPaginationChange" when next clicked', () => { const mockOnPaginationChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); @@ -134,17 +122,15 @@ describe('ExceptionsViewerPagination', () => { it('it invokes "onPaginationChange" when page clicked', () => { const mockOnPaginationChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx index 6167a29a4a17d..42ce0c792dfa3 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx @@ -8,14 +8,25 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from '@kbn/test/jest'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { ExceptionsViewerUtility } from './exceptions_utility'; +const mockTheme = { + eui: { + euiBreakpoints: { + l: '1200px', + }, + paddingSizes: { + m: '10px', + }, + euiBorderThin: '1px solid #ece', + }, +}; + describe('ExceptionsViewerUtility', () => { it('it renders correct pluralized text when more than one exception exists', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders correct singular text when less than two exceptions exists', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it invokes "onRefreshClick" when refresh button clicked', () => { const mockOnRefreshClick = jest.fn(); const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does not render any messages when "showEndpointList" and "showDetectionsList" are "false"', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does render detections messages when "showDetectionsList" is "true"', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does render endpoint messages when "showEndpointList" is "true"', () => { const wrapper = mountWithIntl( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders all disabled if "isInitLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -47,16 +43,14 @@ describe('ExceptionsViewerHeader', () => { it('it displays toggles and add exception popover when more than one list type available', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]').exists()).toBeTruthy(); @@ -67,16 +61,14 @@ describe('ExceptionsViewerHeader', () => { it('it does not display toggles and add exception popover if only one list type is available', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]')).toHaveLength(0); @@ -87,16 +79,14 @@ describe('ExceptionsViewerHeader', () => { it('it displays add exception button without popover if only one list type is available', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect( @@ -107,16 +97,14 @@ describe('ExceptionsViewerHeader', () => { it('it renders detections filter toggle selected when clicked', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').simulate('click'); @@ -149,16 +137,14 @@ describe('ExceptionsViewerHeader', () => { it('it renders endpoint filter toggle selected and invokes "onFilterChange" when clicked', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').simulate('click'); @@ -191,16 +177,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddExceptionClick" when user selects to add an exception item and only endpoint exception lists are available', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); @@ -211,16 +195,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item and only endpoint detections lists are available', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); @@ -231,16 +213,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddEndpointExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper @@ -254,16 +234,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { const mockOnAddExceptionClick = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper @@ -277,16 +255,14 @@ describe('ExceptionsViewerHeader', () => { it('it invokes "onFilterChange" when search used and "Enter" pressed', () => { const mockOnFilterChange = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('EuiFieldSearch').at(0).simulate('keyup', { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 3171735d905de..167b95995212b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -8,27 +8,32 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import * as i18n from '../translations'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; +const mockTheme = { + eui: { + euiSize: '10px', + euiColorPrimary: '#ece', + euiColorDanger: '#ece', + }, +}; + describe('ExceptionsViewerItems', () => { it('it renders empty prompt if "showEmpty" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); @@ -43,7 +48,7 @@ describe('ExceptionsViewerItems', () => { it('it renders no search results found prompt if "showNoResults" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it does not render exceptions if "isInitLoading" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { exception2.id = 'newId'; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { exception2.id = 'newId'; const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { const mockOnDeleteException = jest.fn(); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { jest.fn(), ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { it('it renders empty prompt if no "exceptionListMeta" passed in', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const closeModal = jest.fn(); describe('rendering', () => { test('when isShowing is positive and request and response are not null', () => { const wrapper = mount( - + { describe('functionality from tab statistics/request/response', () => { test('Click on statistic Tab', () => { const wrapper = mount( - + { test('Click on request Tab', () => { const wrapper = mount( - + { test('Click on response Tab', () => { const wrapper = mount( - + { describe('events', () => { test('Make sure that toggle function has been called when you click on the close button', () => { const wrapper = mount( - + - +

My test supplement.

- } - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={ - Array [ - Object { - "numberOfRow": 2, - "text": "2 rows", - }, - Object { - "numberOfRow": 5, - "text": "5 rows", - }, - Object { - "numberOfRow": 10, - "text": "10 rows", - }, - Object { - "numberOfRow": 20, - "text": "20 rows", - }, - Object { - "numberOfRow": 50, - "text": "50 rows", - }, - ] - } - limit={1} - loadPage={[MockFunction]} - loading={false} - pageOfItems={ - Array [ - Object { - "cursor": Object { - "value": "98966fa2013c396155c460d35c0902be", - }, - "host": Object { - "_id": "cPsuhGcB0WOhS6qyTKC0", - "firstSeen": "2018-12-06T15:40:53.319Z", - "name": "elrond.elstc.co", - "os": "Ubuntu", - "version": "18.04.1 LTS (Bionic Beaver)", - }, - }, - Object { - "cursor": Object { - "value": "aa7ca589f1b8220002f2fc61c64cfbf1", - }, - "host": Object { - "_id": "KwQDiWcB0WOhS6qyXmrW", - "firstSeen": "2018-12-07T14:12:38.560Z", - "name": "siem-kibana", - "os": "Debian GNU/Linux", - "version": "9 (stretch)", - }, - }, - ] - } - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={[MockFunction]} - updateLimitPagination={[Function]} - /> - +
+ + + + + Rows per page: 1 + + } + closePopover={[Function]} + data-test-subj="loadingMoreSizeRowPopover" + display="inlineBlock" + hasArrow={true} + id="customizablePagination" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + repositionOnScroll={true} + > + + 2 rows +
, + + 5 rows + , + + 10 rows + , + + 20 rows + , + + 50 rows + , + ] + } + /> + + + + + + + + `; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 57d4c8451de24..c20f1ae66c797 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -14,7 +14,6 @@ import { Direction } from '../../../graphql/types'; import { BasicTableProps, PaginatedTable } from './index'; import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; jest.mock('react', () => { const r = jest.requireActual('react'); @@ -22,8 +21,20 @@ jest.mock('react', () => { return { ...r, memo: (x: any) => x }; }); +const mockTheme = { + eui: { + euiColorEmptyShade: '#ece', + euiSizeL: '10px', + euiBreakpoints: { + s: '450px', + }, + paddingSizes: { + m: '10px', + }, + }, +}; + describe('Paginated Table Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let loadPage: jest.Mock; let updateLimitPagination: jest.Mock; let updateActivePage: jest.Mock; @@ -36,26 +47,24 @@ describe('Paginated Table Component', () => { describe('rendering', () => { test('it renders the default load more table', () => { const wrapper = shallow( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> -
+ {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={(limit) => updateLimitPagination({ limit })} + /> ); expect(wrapper).toMatchSnapshot(); @@ -63,7 +72,7 @@ describe('Paginated Table Component', () => { test('it renders the loading panel at the beginning ', () => { const wrapper = mount( - + { test('it renders the over loading panel after data has been in the table ', () => { const wrapper = mount( - + { test('it renders the correct amount of pages and starts at activePage: 0', () => { const wrapper = mount( - + { test('it render popover to select new limit in table', () => { const wrapper = mount( - + { test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { const wrapper = mount( - + { test('It should render a sort icon if sorting is defined', () => { const mockOnChange = jest.fn(); const wrapper = mount( - + { test('Should display toast when user reaches end of results max', () => { const wrapper = mount( - + { test('Should show items per row if totalCount is greater than items', () => { const wrapper = mount( - + { test('Should hide items per row if totalCount is less than items', () => { const wrapper = mount( - + { describe('Events', () => { test('should call updateActivePage with 1 when clicking to the first page', () => { const wrapper = mount( - + { test('Should call updateActivePage with 0 when you pick a new limit', () => { const wrapper = mount( - + { // eslint-disable-next-line @typescript-eslint/no-explicit-any const ComponentWithContext = (props: BasicTableProps) => { return ( - + ); @@ -424,7 +433,7 @@ describe('Paginated Table Component', () => { test('Should call updateLimitPagination when you pick a new limit', () => { const wrapper = mount( - + { test('Should call onChange when you choose a new sort in the table', () => { const mockOnChange = jest.fn(); const wrapper = mount( - + { }); describe('Stat Items Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const store = createStore( @@ -71,7 +70,7 @@ describe('Stat Items Component', () => { describe.each([ [ mount( - + { ], [ mount( - + { test('it renders entryItemIndexItemEntryFirstRowAndBadge for very first item', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -25,7 +30,7 @@ describe('AndBadgeComponent', () => { test('it renders entryItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); @@ -37,7 +42,7 @@ describe('AndBadgeComponent', () => { test('it renders regular "and" badge if item is not the first one and includes more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx index 73174bc5fc113..6aa33c3bcf4ca 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { waitFor } from '@testing-library/react'; import { fields } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; @@ -19,6 +18,12 @@ import { ThreatMatchComponent } from './'; import { ThreatMapEntries } from './types'; import { IndexPattern } from 'src/plugins/data/public'; +const mockTheme = { + eui: { + euiColorLightShade: '#ece', + }, +}; + jest.mock('../../../common/lib/kibana'); const getPayLoad = (): ThreatMapEntries[] => [ @@ -51,7 +56,7 @@ describe('ThreatMatchComponent', () => { test('it displays empty entry if no "listItems" are passed in', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "Search" for "listItems" that are passed in', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "or", "and" enabled', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an entry when "and" clicked', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it adds an item when "or" clicked', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it removes one row if user deletes a row', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it displays "and" badge if at least one item includes more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it does not display "and" badge if none of the items include more than one entry', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + ({ @@ -66,7 +71,7 @@ describe('ListItemComponent', () => { describe('and badge logic', () => { test('it renders "and" badge with extra top padding for the first item when "andLogicIncluded" is "true"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders "and" badge when more than one item entry exists and it is not the first item', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders indented "and" badge when "andLogicIncluded" is "true" and only one entry exists', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders no "and" badge when "andLogicIncluded" is "false"', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders loader when isLoading is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -58,23 +54,21 @@ describe('PreviewCustomQueryHistogram', () => { test('it configures data and subtitle', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); @@ -111,19 +105,17 @@ describe('PreviewCustomQueryHistogram', () => { const mockRefetch = jest.fn(); mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(mockSetQuery).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx index 65bb029e2e32f..df6a8975a5b97 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; @@ -35,19 +33,17 @@ describe('PreviewEqlQueryHistogram', () => { test('it renders loader when isLoading is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -58,23 +54,21 @@ describe('PreviewEqlQueryHistogram', () => { test('it configures data and subtitle', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); @@ -109,19 +103,17 @@ describe('PreviewEqlQueryHistogram', () => { const mockRefetch = jest.fn(); mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(mockSetQuery).toHaveBeenCalledWith({ @@ -134,23 +126,21 @@ describe('PreviewEqlQueryHistogram', () => { test('it displays histogram', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx index d9bd32ce082ca..85e31e7ed36e5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx @@ -6,9 +6,7 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { TestProviders } from '../../../../common/mock'; import { PreviewHistogram } from './histogram'; @@ -17,19 +15,17 @@ import { getHistogramConfig } from './helpers'; describe('PreviewHistogram', () => { test('it renders loading icon if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -38,40 +34,38 @@ describe('PreviewHistogram', () => { test('it renders chart if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx index bb87242d9bf10..700c2d516b995 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { of } from 'rxjs'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { TestProviders } from '../../../../common/mock'; import { useKibana } from '../../../../common/lib/kibana'; @@ -18,6 +17,12 @@ import { getMockEqlResponse } from '../../../../common/hooks/eql/eql_search_resp import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; import { useEqlPreview } from '../../../../common/hooks/eql/'; +const mockTheme = { + eui: { + euiSuperDatePickerWidth: '180px', + }, +}; + jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/containers/matrix_histogram'); jest.mock('../../../../common/hooks/eql/'); @@ -63,7 +68,7 @@ describe('PreviewQuery', () => { test('it renders timeframe select and preview button on render', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders preview button disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders preview button disabled if "query" is undefined', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when rule type is query and preview button clicked', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when rule type is saved_query and preview button clicked', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders eql histogram when preview button clicked and rule type is eql', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it hides histogram when timeframe changes', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> + { test('it renders loader when isLoading is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); @@ -51,20 +47,18 @@ describe('PreviewThresholdQueryHistogram', () => { test('it configures buckets data', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); @@ -86,20 +80,18 @@ describe('PreviewThresholdQueryHistogram', () => { const mockRefetch = jest.fn(); mount( - ({ eui: euiLightVars, darkMode: false })}> - - - - + + + ); expect(mockSetQuery).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx index 71670658c88a9..fc91c26148c17 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_switch/index.test.tsx @@ -7,8 +7,6 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { waitFor } from '@testing-library/react'; import { enableRules } from '../../../containers/detection_engine/rules'; @@ -34,9 +32,7 @@ describe('RuleSwitch', () => { test('it renders loader if "isLoading" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="ruleSwitchLoader"]').exists()).toBeTruthy(); @@ -45,42 +41,27 @@ describe('RuleSwitch', () => { test('it renders switch disabled if "isDisabled" is true', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().disabled).toBeTruthy(); }); test('it renders switch enabled if "enabled" is true', () => { - const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - - ); + const wrapper = mount(); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeTruthy(); }); test('it renders switch disabled if "enabled" is false', () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); expect(wrapper.find('[data-test-subj="ruleSwitch"]').at(0).props().checked).toBeFalsy(); }); test('it renders an off switch enabled on click', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -96,9 +77,7 @@ describe('RuleSwitch', () => { (enableRules as jest.Mock).mockResolvedValue([rule]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -113,14 +92,7 @@ describe('RuleSwitch', () => { (enableRules as jest.Mock).mockRejectedValue(mockError); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -138,14 +110,7 @@ describe('RuleSwitch', () => { ]); const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); @@ -157,15 +122,13 @@ describe('RuleSwitch', () => { test('it invokes "enableRulesAction" if dispatch is passed through', async () => { const wrapper = mount( - ({ eui: euiLightVars, darkMode: false })}> - - + ); wrapper.find('[data-test-subj="ruleSwitch"]').at(2).simulate('click'); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index edd5c0d4e6e4c..c1773b2fffbab 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; import { act } from '@testing-library/react'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; @@ -24,8 +23,13 @@ import { } from '../../../pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers'; +const mockTheme = { + eui: { + euiColorLightestShade: '#ece', + }, +}; + jest.mock('../../../../common/containers/source'); -const theme = () => ({ eui: euiDarkVars, darkMode: true }); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { @@ -37,6 +41,7 @@ jest.mock('@elastic/eui', () => { }, }; }); + describe('StepAboutRuleComponent', () => { let formHook: RuleStepsFormHooks[RuleStep.aboutRule] | null = null; const setFormHook = ( @@ -72,7 +77,7 @@ describe('StepAboutRuleComponent', () => { it('is invalid if description is not present', async () => { const wrapper = mount( - + { it('is invalid if no "name" is present', async () => { const wrapper = mount( - + { it('is valid if both "name" and "description" are present', async () => { const wrapper = mount( - + { it('it allows user to set the risk score as a number (and not a string)', async () => { const wrapper = mount( - + { it('does not modify the provided risk score until the user changes the severity', async () => { const wrapper = mount( - + ({ eui: euiDarkVars, darkMode: true }); +const mockTheme = { + eui: { euiSizeL: '10px', euiBreakpoints: { s: '450px' }, paddingSizes: { m: '10px' } }, +}; describe('StepAboutRuleToggleDetails', () => { let mockRule: AboutStepRule; @@ -93,7 +94,7 @@ describe('StepAboutRuleToggleDetails', () => { describe('note value does exist', () => { test('it renders toggle buttons, defaulted to "details"', () => { const wrapper = mount( - + { test('it allows users to toggle between "details" and "note"', () => { const wrapper = mount( - + { test('it displays notes markdown when user toggles to "notes"', () => { const wrapper = mount( - + { >(() => useQueryAlerts(mockAlertsQuery, indexName)); await waitForNextUpdate(); expect(result.current).toEqual({ - loading: true, + loading: false, data: null, response: '', request: '', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index 8557e1082c1cb..3736c8593daa9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -42,7 +42,7 @@ export const useQueryAlerts = ( setQuery, refetch: null, }); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); useEffect(() => { let isSubscribed = true; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx index a17a6d8318a3b..687142cc7ed17 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx @@ -8,17 +8,16 @@ import React from 'react'; import { mount } from 'enzyme'; import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { waitFor } from '@testing-library/react'; import { AllRulesUtilityBar } from './utility_bar'; -const theme = () => ({ eui: euiDarkVars, darkMode: true }); +const mockTheme = { eui: { euiBreakpoints: { l: '1200px' }, paddingSizes: { m: '10px' } } }; describe('AllRules', () => { it('renders AllRulesUtilityBar total rules and selected rules', () => { const wrapper = mount( - + { it('does not render total selected and bulk actions when "showBulkActions" is false', () => { const wrapper = mount( - + { it('renders utility actions if user has permissions', () => { const wrapper = mount( - + { it('renders no utility actions if user has no permissions', () => { const wrapper = mount( - + { it('invokes refresh on refresh action click', () => { const mockRefresh = jest.fn(); const wrapper = mount( - + { it('invokes onRefreshSwitch when auto refresh switch is clicked', async () => { const mockSwitch = jest.fn(); const wrapper = mount( - + ({ htmlIdGenerator: () => () => 'mockId', })); @@ -32,9 +37,7 @@ const now = 111111; const renderList = (store: ReturnType) => { const Wrapper: React.FC = ({ children }) => ( - ({ eui: euiLightVars, darkMode: false })}> - {children} - + {children} ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 8f70c61ba4afc..5a176018f0e3f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -486,12 +486,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the seco exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` .c0 { - background-color: #f5f7fa; - padding: 16px; -} - -.c3 { - padding: 16px; + background-color: #ece; } .c1.c1.c1 { @@ -864,7 +859,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
({ @@ -32,9 +37,7 @@ const now = 111111; const renderList = (store: ReturnType) => { const Wrapper: React.FC = ({ children }) => ( - ({ eui: euiLightVars, darkMode: false })}> - {children} - + {children} ); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 4b62139a8679f..0ec12a00d578b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -5,10 +5,8 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../../common/mock/match_media'; import '../../../common/mock/react_beautiful_dnd'; @@ -24,7 +22,6 @@ jest.mock('../../../common/containers/matrix_histogram', () => ({ useMatrixHistogram: jest.fn(), })); -const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); const from = '2020-03-31T06:00:00.000Z'; const to = '2019-03-31T06:00:00.000Z'; @@ -55,11 +52,9 @@ describe('Alerts by category', () => { ]); wrapper = mount( - - - - - + + + ); await waitFor(() => { @@ -123,11 +118,9 @@ describe('Alerts by category', () => { ]); wrapper = mount( - - - - - + + + ); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx index ae8a00d2b4aa0..004e675cb3516 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx @@ -113,7 +113,7 @@ const StatefulRecentTimelinesComponent: React.FC = ({ apolloClient, filte )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx index 278e01bcd8923..0f7a2070b8ef4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.test.tsx @@ -7,8 +7,6 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mockBrowserFields } from '../../../common/containers/source/mock'; @@ -19,18 +17,15 @@ import * as i18n from './translations'; const timelineId = 'test'; describe('CategoriesPane', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); test('it renders the expected title', () => { const wrapper = mount( - - - + ); expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( @@ -40,15 +35,13 @@ describe('CategoriesPane', () => { test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { const wrapper = mount( - - - + ); expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx index 44b65185627ff..7b00b768b56a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.test.tsx @@ -12,25 +12,20 @@ import { mockBrowserFields } from '../../../common/containers/source/mock'; import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers'; import { CategoriesPane } from './categories_pane'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; const timelineId = 'test'; -const theme = () => ({ eui: euiDarkVars, darkMode: true }); describe('getCategoryColumns', () => { Object.keys(mockBrowserFields).forEach((categoryId) => { test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { const wrapper = mount( - - - + ); const fieldCount = Object.keys(mockBrowserFields[categoryId].fields ?? {}).length; @@ -44,15 +39,13 @@ describe('getCategoryColumns', () => { Object.keys(mockBrowserFields).forEach((categoryId) => { test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { const wrapper = mount( - - - + ); expect( @@ -65,15 +58,13 @@ describe('getCategoryColumns', () => { const selectedCategoryId = 'auditd'; const wrapper = mount( - - - + ); expect( @@ -89,15 +80,13 @@ describe('getCategoryColumns', () => { const notTheSelectedCategoryId = 'base'; const wrapper = mount( - - - + ); expect( @@ -115,15 +104,13 @@ describe('getCategoryColumns', () => { const onCategorySelected = jest.fn(); const wrapper = mount( - - - + ); wrapper diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 487d42aac4840..5cba64299ee9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -20,7 +20,7 @@ import { TimelineStatus, TimelineId, TimelineType } from '../../../../../common/ import { getCreateCaseUrl, getCaseDetailsUrl } from '../../../../common/components/link_to'; import { SecurityPageName } from '../../../../app/types'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; -import { Case } from '../../../../cases/containers/types'; +import { Case, SubCase } from '../../../../cases/containers/types'; import * as i18n from '../../timeline/properties/translations'; interface Props { @@ -46,7 +46,7 @@ const AddToCaseButtonComponent: React.FC = ({ timelineId }) => { const [isPopoverOpen, setPopover] = useState(false); const onRowClick = useCallback( - async (theCase?: Case) => { + async (theCase?: Case | SubCase) => { await navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(), }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx index 49f68281ae103..c130ea4c96814 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx @@ -7,12 +7,26 @@ import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; +import { useParams } from 'react-router-dom'; import { DeleteTimelineModal } from './delete_timeline_modal'; import * as i18n from '../translations'; +import { TimelineType } from '../../../../../common/types/timeline'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useParams: jest.fn(), + }; +}); describe('DeleteTimelineModal', () => { + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + }); + test('it renders the expected title when a timeline is selected', () => { const wrapper = mountWithIntl( { /> ); - expect(wrapper.find('[data-test-subj="warning"]').first().text()).toEqual(i18n.DELETE_WARNING); + expect(wrapper.find('[data-test-subj="warning"]').first().text()).toEqual( + i18n.DELETE_TIMELINE_WARNING + ); }); test('it invokes closeModal when the Cancel button is clicked', () => { @@ -115,3 +131,23 @@ describe('DeleteTimelineModal', () => { expect(onDelete).toBeCalled(); }); }); + +describe('DeleteTimelineTemplateModal', () => { + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.template }); + }); + + test('it renders a deletion warning', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="warning"]').first().text()).toEqual( + i18n.DELETE_TIMELINE_TEMPLATE_WARNING + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx index 5538610487899..f0efda6528507 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx @@ -10,7 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; +import { useParams } from 'react-router-dom'; import * as i18n from '../translations'; +import { TimelineType } from '../../../../../common/types/timeline'; interface Props { title?: string | null; @@ -24,6 +26,12 @@ export const DELETE_TIMELINE_MODAL_WIDTH = 600; // px * Renders a modal that confirms deletion of a timeline */ export const DeleteTimelineModal = React.memo(({ title, closeModal, onDelete }) => { + const { tabName } = useParams<{ tabName: TimelineType }>(); + const warning = + tabName === TimelineType.template + ? i18n.DELETE_TIMELINE_TEMPLATE_WARNING + : i18n.DELETE_TIMELINE_WARNING; + const getTitle = useCallback(() => { const trimmedTitle = title != null ? title.trim() : ''; const titleResult = !isEmpty(trimmedTitle) ? trimmedTitle : i18n.UNTITLED_TIMELINE; @@ -48,7 +56,7 @@ export const DeleteTimelineModal = React.memo(({ title, closeModal, onDel onConfirm={onDelete} title={getTitle()} > -
{i18n.DELETE_WARNING}
+
{warning}
); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx index 18a6ffc06941c..cfbc7d255062f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx @@ -7,8 +7,18 @@ import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; +import { useParams } from 'react-router-dom'; import { DeleteTimelineModalOverlay } from '.'; +import { TimelineType } from '../../../../../common/types/timeline'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useParams: jest.fn(), + }; +}); describe('DeleteTimelineModal', () => { const savedObjectId = 'abcd'; @@ -20,6 +30,10 @@ describe('DeleteTimelineModal', () => { title: 'Privilege Escalation', }; + beforeAll(() => { + (useParams as jest.Mock).mockReturnValue({ tabName: TimelineType.default }); + }); + describe('showModalState', () => { test('it does NOT render the modal when isModalOpen is false', () => { const testProps = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index 8e754b3d04654..0c611ca5106e8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import moment from 'moment'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import '../../../../common/mock/formatted_relative'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; @@ -18,7 +16,6 @@ import { OpenTimelineResult, TimelineResultNote } from '../types'; import { NotePreviews } from '.'; describe('NotePreviews', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; let note1updated: number; let note2updated: number; @@ -34,11 +31,7 @@ describe('NotePreviews', () => { test('it renders a note preview for each note when isModal is false', () => { const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); hasNotes[0].notes!.forEach(({ savedObjectId }) => { expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); @@ -48,11 +41,7 @@ describe('NotePreviews', () => { test('it renders a note preview for each note when isModal is true', () => { const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); hasNotes[0].notes!.forEach(({ savedObjectId }) => { expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); @@ -99,11 +88,7 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('.euiCommentEvent__headerUsername').at(1).text()).toEqual('bob'); }); @@ -130,11 +115,7 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); @@ -160,11 +141,7 @@ describe('NotePreviews', () => { }, ]; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 1aac1f21f2d50..0cf7f2891dfbf 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -32,8 +31,19 @@ jest.mock('react-router-dom', () => { }; }); +const mockTheme = { + eui: { + euiSizeL: '10px', + paddingSizes: { + s: '10px', + }, + euiBreakpoints: { + l: '1200px', + }, + }, +}; + describe('OpenTimeline', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; let mockResults: OpenTimelineResult[]; @@ -73,7 +83,7 @@ describe('OpenTimeline', () => { test('it renders the search row', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -84,7 +94,7 @@ describe('OpenTimeline', () => { test('it renders the timelines table', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -95,7 +105,7 @@ describe('OpenTimeline', () => { test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -115,7 +125,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -135,7 +145,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -155,7 +165,7 @@ describe('OpenTimeline', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -174,7 +184,7 @@ describe('OpenTimeline', () => { query: '', }; const wrapper = mountWithIntl( - + ); @@ -188,7 +198,7 @@ describe('OpenTimeline', () => { query: ' ', }; const wrapper = mountWithIntl( - + ); @@ -202,7 +212,7 @@ describe('OpenTimeline', () => { query: 'Would you like to go to Denver?', }; const wrapper = mountWithIntl( - + ); @@ -218,7 +228,7 @@ describe('OpenTimeline', () => { query: ' Is it starting to feel cramped in here? ', }; const wrapper = mountWithIntl( - + ); @@ -234,7 +244,7 @@ describe('OpenTimeline', () => { query: '', }; const wrapper = mountWithIntl( - + ); @@ -250,7 +260,7 @@ describe('OpenTimeline', () => { query: ' ', }; const wrapper = mountWithIntl( - + ); @@ -266,7 +276,7 @@ describe('OpenTimeline', () => { query: 'How was your day?', }; const wrapper = mountWithIntl( - + ); @@ -282,7 +292,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.active, }; const wrapper = mountWithIntl( - + ); @@ -297,7 +307,7 @@ describe('OpenTimeline', () => { selectedItems: [], }; const wrapper = mountWithIntl( - + ); @@ -317,7 +327,7 @@ describe('OpenTimeline', () => { selectedItems: [], }; const wrapper = mountWithIntl( - + ); @@ -337,7 +347,7 @@ describe('OpenTimeline', () => { selectedItems: [{}], }; const wrapper = mountWithIntl( - + ); @@ -357,7 +367,7 @@ describe('OpenTimeline', () => { selectedItems: [{}], }; const wrapper = mountWithIntl( - + ); @@ -376,7 +386,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.active, }; const wrapper = mountWithIntl( - + ); @@ -392,7 +402,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.active, }; const wrapper = mountWithIntl( - + ); @@ -406,7 +416,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.immutable, }; const wrapper = mountWithIntl( - + ); @@ -420,7 +430,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.immutable, }; const wrapper = mountWithIntl( - + ); @@ -436,7 +446,7 @@ describe('OpenTimeline', () => { timelineStatus: TimelineStatus.immutable, }; const wrapper = mountWithIntl( - + ); @@ -450,7 +460,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; const wrapper = mountWithIntl( - + ); @@ -464,7 +474,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; const wrapper = mountWithIntl( - + ); @@ -480,7 +490,7 @@ describe('OpenTimeline', () => { timelineStatus: null, }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 24b0702770d3c..5a3da748bea1d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -122,9 +122,9 @@ export const OpenTimeline = React.memo( const onRefreshBtnClick = useCallback(() => { if (refetch != null) { - refetch(searchResults, totalSearchResultsCount); + refetch(); } - }, [refetch, searchResults, totalSearchResultsCount]); + }, [refetch]); const handleCloseModal = useCallback(() => { if (setImportDataModalToggle != null) { @@ -137,9 +137,9 @@ export const OpenTimeline = React.memo( setImportDataModalToggle(false); } if (refetch != null) { - refetch(searchResults, totalSearchResultsCount); + refetch(); } - }, [setImportDataModalToggle, refetch, searchResults, totalSearchResultsCount]); + }, [setImportDataModalToggle, refetch]); const actionTimelineToShow = useMemo(() => { const timelineActions: ActionTimelineToShow[] = ['createFrom', 'duplicate']; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 5babecb3acb69..38186d35d2d2d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -23,7 +22,14 @@ import { TimelineType, TimelineStatus } from '../../../../../common/types/timeli jest.mock('../../../../common/lib/kibana'); describe('OpenTimelineModal', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mockTheme = { + eui: { + euiColorMediumShade: '#ece', + euiBreakpoints: { + s: '500px', + }, + }, + }; const title = 'All Timelines / Open Timelines'; let mockResults: OpenTimelineResult[]; @@ -62,7 +68,7 @@ describe('OpenTimelineModal', () => { test('it renders the title row', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -73,7 +79,7 @@ describe('OpenTimelineModal', () => { test('it renders the search row', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -84,7 +90,7 @@ describe('OpenTimelineModal', () => { test('it renders the timelines table', () => { const defaultProps = getDefaultTestProps(mockResults); const wrapper = mountWithIntl( - + ); @@ -99,7 +105,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: jest.fn(), }; const wrapper = mountWithIntl( - + ); @@ -119,7 +125,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -139,7 +145,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); @@ -159,7 +165,7 @@ describe('OpenTimelineModal', () => { deleteTimelines: undefined, }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx index 837dcbe1d6bfd..62cdda6070b32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { ThemeProvider } from 'styled-components'; import { waitFor } from '@testing-library/react'; import { TestProviders } from '../../../../common/mock/test_providers'; @@ -19,8 +17,6 @@ import * as i18n from '../translations'; import { OpenTimelineModalButton } from './open_timeline_modal_button'; describe('OpenTimelineModalButton', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - test('it renders the expected button text', async () => { const wrapper = mount( @@ -43,13 +39,11 @@ describe('OpenTimelineModalButton', () => { test('it invokes onClick function provided as a prop when the button is clicked', async () => { const onClick = jest.fn(); const wrapper = mount( - - - - - - - + + + + + ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx index e0b252e112fc6..d75823b771681 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.test.tsx @@ -6,7 +6,6 @@ */ import { EuiFilterButtonProps } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -17,12 +16,16 @@ import { SearchRow } from '.'; import * as i18n from '../translations'; -describe('SearchRow', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); +const mockTheme = { + eui: { + euiSizeL: '10px', + }, +}; +describe('SearchRow', () => { test('it renders a search input with the expected placeholder when the query is empty', () => { const wrapper = mountWithIntl( - + { describe('Only Favorites Button', () => { test('it renders the expected button text', () => { const wrapper = mountWithIntl( - + { const onToggleOnlyFavorites = jest.fn(); const wrapper = mountWithIntl( - + { test('it sets the button to the toggled state when onlyFavorites is true', () => { const wrapper = mountWithIntl( - + { test('it sets the button to the NON-toggled state when onlyFavorites is false', () => { const wrapper = mountWithIntl( - + { test('it invokes onQueryChange when the user enters a query', () => { const wrapper = mountWithIntl( - + { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -37,7 +37,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['delete'], }; const wrapper = mountWithIntl( - + ); @@ -51,7 +51,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: [], }; const wrapper = mountWithIntl( - + ); @@ -65,7 +65,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -79,7 +79,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -93,7 +93,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: [], }; const wrapper = mountWithIntl( - + ); @@ -107,7 +107,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['delete'], }; const wrapper = mountWithIntl( - + ); @@ -124,7 +124,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -141,7 +141,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -161,7 +161,7 @@ describe('#getActionsColumns', () => { onOpenTimeline, }; const wrapper = mountWithIntl( - + ); @@ -177,7 +177,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['export'], }; const wrapper = mountWithIntl( - + ); @@ -189,7 +189,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -206,7 +206,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['export'], }; const wrapper = mountWithIntl( - + ); @@ -226,7 +226,7 @@ describe('#getActionsColumns', () => { enableExportTimelineDownloader, }; const wrapper = mountWithIntl( - + ); @@ -242,7 +242,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['createFrom', 'duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -256,7 +256,7 @@ describe('#getActionsColumns', () => { actionTimelineToShow: ['createFrom', 'duplicate'], }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index 3108dd09ea687..3c70cc70a66de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -6,7 +6,6 @@ */ import { EuiButtonIconProps } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -23,10 +22,11 @@ import { TimelinesTable, TimelinesTableProps } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('#getCommonColumns', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -40,11 +40,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(hasNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(true); }); @@ -55,11 +51,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(missingNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -70,11 +62,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(nullNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -85,11 +73,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(emptylNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -101,11 +85,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(missingSavedObjectId), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -116,11 +96,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(nullSavedObjectId), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="expand-notes"]').exists()).toBe(false); }); @@ -131,11 +107,7 @@ describe('#getCommonColumns', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(hasNotes), }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); const props = wrapper .find('[data-test-subj="expand-notes"]') @@ -156,11 +128,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(hasNotes), itemIdToExpandedNotesRowMap, }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); const props = wrapper .find('[data-test-subj="expand-notes"]') @@ -184,11 +152,7 @@ describe('#getCommonColumns', () => { itemIdToExpandedNotesRowMap, onToggleShowNotes, }; - const wrapper = mountWithIntl( - - - - ); + const wrapper = mountWithIntl(); wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); @@ -214,7 +178,7 @@ describe('#getCommonColumns', () => { onToggleShowNotes, }; const wrapper = mountWithIntl( - + ); @@ -233,7 +197,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -246,7 +210,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -265,7 +229,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -285,7 +249,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingTitle), }; const wrapper = mountWithIntl( - + ); @@ -304,7 +268,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withMissingSavedObjectIdAndTitle), }; const wrapper = mountWithIntl( - + ); @@ -323,7 +287,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withJustWhitespaceTitle), }; const wrapper = mountWithIntl( - + ); @@ -345,7 +309,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(withMissingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -360,7 +324,7 @@ describe('#getCommonColumns', () => { test('it renders a hyperlink when the timeline has a saved object id', () => { const wrapper = mountWithIntl( - + ); @@ -379,7 +343,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(missingSavedObjectId), }; const wrapper = mountWithIntl( - + ); @@ -397,7 +361,7 @@ describe('#getCommonColumns', () => { onOpenTimeline, }; const wrapper = mountWithIntl( - + ); @@ -417,7 +381,7 @@ describe('#getCommonColumns', () => { describe('Description column', () => { test('it renders the expected column name', () => { const wrapper = mountWithIntl( - + ); @@ -427,7 +391,7 @@ describe('#getCommonColumns', () => { test('it renders the description when the timeline has a description', () => { const wrapper = mountWithIntl( - + ); @@ -441,7 +405,7 @@ describe('#getCommonColumns', () => { const missingDescription: OpenTimelineResult[] = [omit('description', { ...mockResults[0] })]; const wrapper = mountWithIntl( - + ); @@ -459,7 +423,7 @@ describe('#getCommonColumns', () => { ...getMockTimelinesTableProps(justWhitespaceDescription), }; const wrapper = mountWithIntl( - + ); @@ -472,7 +436,7 @@ describe('#getCommonColumns', () => { describe('Last Modified column', () => { test('it renders the expected column name', () => { const wrapper = mountWithIntl( - + ); @@ -482,7 +446,7 @@ describe('#getCommonColumns', () => { test('it renders the last modified (updated) date when the timeline has an updated property', () => { const wrapper = mountWithIntl( - + ); @@ -497,7 +461,7 @@ describe('#getCommonColumns', () => { const missingUpdated: OpenTimelineResult[] = [omit('updated', { ...mockResults[0] })]; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx index 66556296c42ac..83e21267bce28 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -21,10 +20,11 @@ import { TimelinesTable, TimelinesTableProps } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('#getExtendedColumns', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -37,7 +37,7 @@ describe('#getExtendedColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -50,7 +50,7 @@ describe('#getExtendedColumns', () => { ...getMockTimelinesTableProps(mockResults), }; const wrapper = mountWithIntl( - + ); @@ -66,7 +66,7 @@ describe('#getExtendedColumns', () => { ...getMockTimelinesTableProps(missingUpdatedBy), }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx index c3681753c7732..a8ed5f02fa3ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep, omit } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -16,10 +15,12 @@ import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { TimelinesTable, TimelinesTableProps } from '.'; import { OpenTimelineResult } from '../types'; import { getMockTimelinesTableProps } from './mocks'; + +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('#getActionsColumns', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -28,7 +29,7 @@ describe('#getActionsColumns', () => { test('it renders the pinned events header icon', () => { const wrapper = mountWithIntl( - + ); @@ -42,7 +43,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(with6Events), }; const wrapper = mountWithIntl( - + ); @@ -52,7 +53,7 @@ describe('#getActionsColumns', () => { test('it renders the notes count header icon', () => { const wrapper = mountWithIntl( - + ); @@ -66,7 +67,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(with4Notes), }; const wrapper = mountWithIntl( - + ); @@ -76,7 +77,7 @@ describe('#getActionsColumns', () => { test('it renders the favorites header icon', () => { const wrapper = mountWithIntl( - + ); @@ -90,7 +91,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(undefinedFavorite), }; const wrapper = mountWithIntl( - + ); @@ -104,7 +105,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(nullFavorite), }; const wrapper = mountWithIntl( - + ); @@ -118,7 +119,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(emptyFavorite), }; const wrapper = mountWithIntl( - + ); @@ -143,7 +144,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(favorite), }; const wrapper = mountWithIntl( - + ); @@ -172,7 +173,7 @@ describe('#getActionsColumns', () => { ...getMockTimelinesTableProps(favorite), }; const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx index 2d5949ae41125..01a855524ac0d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -19,10 +18,11 @@ import { getMockTimelinesTableProps } from './mocks'; import * as i18n from '../translations'; +const mockTheme = { eui: { euiColorMediumShade: '#ece' } }; + jest.mock('../../../../common/lib/kibana'); describe('TimelinesTable', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockResults: OpenTimelineResult[]; beforeEach(() => { @@ -31,7 +31,7 @@ describe('TimelinesTable', () => { test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => { const wrapper = mountWithIntl( - + ); @@ -45,7 +45,7 @@ describe('TimelinesTable', () => { actionTimelineToShow: ['delete', 'duplicate'], }; const wrapper = mountWithIntl( - + ); @@ -59,7 +59,7 @@ describe('TimelinesTable', () => { showExtendedColumns: true, }; const wrapper = mountWithIntl( - + ); @@ -73,7 +73,7 @@ describe('TimelinesTable', () => { showExtendedColumns: false, }; const wrapper = mountWithIntl( - + ); @@ -90,7 +90,7 @@ describe('TimelinesTable', () => { test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => { const wrapper = mountWithIntl( - + ); @@ -104,7 +104,7 @@ describe('TimelinesTable', () => { actionTimelineToShow: ['duplicate', 'selectable'], }; const wrapper = mountWithIntl( - + ); @@ -114,7 +114,7 @@ describe('TimelinesTable', () => { test('it renders the rows per page selector when showExtendedColumns is true', () => { const wrapper = mountWithIntl( - + ); @@ -128,7 +128,7 @@ describe('TimelinesTable', () => { showExtendedColumns: false, }; const wrapper = mountWithIntl( - + ); @@ -144,7 +144,7 @@ describe('TimelinesTable', () => { pageSize: defaultPageSize, }; const wrapper = mountWithIntl( - + ); @@ -156,7 +156,7 @@ describe('TimelinesTable', () => { test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => { const wrapper = mountWithIntl( - + ); @@ -170,7 +170,7 @@ describe('TimelinesTable', () => { showExtendedColumns: false, }; const wrapper = mountWithIntl( - + ); @@ -184,7 +184,7 @@ describe('TimelinesTable', () => { searchResults: [], }; const wrapper = mountWithIntl( - + ); @@ -199,7 +199,7 @@ describe('TimelinesTable', () => { onTableChange, }; const wrapper = mountWithIntl( - + ); @@ -221,7 +221,7 @@ describe('TimelinesTable', () => { onSelectionChange, }; const wrapper = mountWithIntl( - + ); @@ -242,7 +242,7 @@ describe('TimelinesTable', () => { loading: true, }; const wrapper = mountWithIntl( - + ); @@ -257,7 +257,7 @@ describe('TimelinesTable', () => { test('it disables the table loading animation when isLoading is false', () => { const wrapper = mountWithIntl( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx index 8c553bb95e9bd..c1b30f3e68cf4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx @@ -25,7 +25,11 @@ import { getActionsColumns } from './actions_columns'; import { getCommonColumns } from './common_columns'; import { getExtendedColumns } from './extended_columns'; import { getIconHeaderColumns } from './icon_header_columns'; -import { TimelineTypeLiteralWithNull, TimelineStatus } from '../../../../../common/types/timeline'; +import { + TimelineTypeLiteralWithNull, + TimelineStatus, + TimelineType, +} from '../../../../../common/types/timeline'; // there are a number of type mismatches across this file const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -103,7 +107,7 @@ export interface TimelinesTableProps { onToggleShowNotes: OnToggleShowNotes; pageIndex: number; pageSize: number; - searchResults: OpenTimelineResult[]; + searchResults: OpenTimelineResult[] | null; showExtendedColumns: boolean; sortDirection: 'asc' | 'desc'; sortField: string; @@ -196,6 +200,13 @@ export const TimelinesTable = React.memo( ] ); + const noItemsMessage = + isLoading || searchResults == null + ? i18n.LOADING + : timelineType === TimelineType.template + ? i18n.ZERO_TIMELINE_TEMPLATES_MATCH + : i18n.ZERO_TIMELINES_MATCH; + return ( ( isSelectable={actionTimelineToShow.includes('selectable')} itemId="savedObjectId" itemIdToExpandedRowMap={itemIdToExpandedNotesRowMap} - items={searchResults} + items={searchResults ?? []} loading={isLoading} - noItemsMessage={i18n.ZERO_TIMELINES_MATCH} + noItemsMessage={noItemsMessage} onChange={onTableChange} pagination={pagination} selection={actionTimelineToShow.includes('selectable') ? selection : undefined} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx index 5621a2287f3a2..4661f72901eb6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/title_row/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { EuiButtonProps } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -13,13 +12,16 @@ import { ThemeProvider } from 'styled-components'; import { TitleRow } from '.'; +const mockTheme = { + eui: { euiSizeS: '10px', euiLineHeight: '20px', euiBreakpoints: { s: '10px' }, euiSize: '10px' }, +}; + describe('TitleRow', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; test('it renders the title', () => { const wrapper = mountWithIntl( - + ); @@ -30,7 +32,7 @@ describe('TitleRow', () => { describe('Favorite Selected button', () => { test('it renders the Favorite Selected button when onAddTimelinesToFavorites is provided', () => { const wrapper = mountWithIntl( - + { test('it does NOT render the Favorite Selected button when onAddTimelinesToFavorites is NOT provided', () => { const wrapper = mountWithIntl( - + ); @@ -54,7 +56,7 @@ describe('TitleRow', () => { test('it disables the Favorite Selected button when the selectedTimelinesCount is 0', () => { const wrapper = mountWithIntl( - + { test('it enables the Favorite Selected button when the selectedTimelinesCount is greater than 0', () => { const wrapper = mountWithIntl( - + { const onAddTimelinesToFavorites = jest.fn(); const wrapper = mountWithIntl( - + export const IMPORT_FAILED = i18n.translate( 'xpack.securitySolution.timelines.components.importTimelineModal.importFailedTitle', { - defaultMessage: 'Failed to import timelines', + defaultMessage: 'Failed to import', } ); export const IMPORT_TIMELINE = i18n.translate( 'xpack.securitySolution.timelines.components.importTimelineModal.importTitle', { - defaultMessage: 'Import timeline…', + defaultMessage: 'Import…', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index ad62bda4c9783..47e1da2d240ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -167,9 +167,9 @@ export interface OpenTimelineProps { /** The currently applied search criteria */ query: string; /** Refetch table */ - refetch?: (existingTimeline?: OpenTimelineResult[], existingCount?: number) => void; - /** The results of executing a search */ - searchResults: OpenTimelineResult[]; + refetch?: () => void; + /** The results of executing a search, null is the status before data fatched */ + searchResults: OpenTimelineResult[] | null; /** the currently-selected timelines in the table */ selectedItems: OpenTimelineResult[]; /** Toggle export timelines modal*/ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx index 88521b779925a..666fb254aaa2c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -5,11 +5,9 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../common/ecs'; @@ -42,11 +40,7 @@ describe('plain_row_renderer', () => { data: mockDatum, timelineId: 'test', }); - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - {children} - - ); + const wrapper = mount({children}); expect(wrapper.text()).toEqual(''); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx index d98a724ebf9cb..6d7a9e5aecfd9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { mount, shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; @@ -17,8 +16,13 @@ import { getEmptyValue } from '../../../../../common/components/empty_value'; import { unknownColumnRenderer } from './unknown_column_renderer'; import { getValues } from './helpers'; +const mockTheme = { + eui: { + euiColorMediumShade: '#ece', + }, +}; + describe('unknown_column_renderer', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; beforeEach(() => { @@ -50,7 +54,7 @@ describe('unknown_column_renderer', () => { timelineId: 'test', }); const wrapper = mount( - + {emptyColumn} ); @@ -66,7 +70,7 @@ describe('unknown_column_renderer', () => { timelineId: 'test', }); const wrapper = mount( - + {emptyColumn} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index d4de77e04e9f7..7ccce80bbe9a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -222,7 +222,7 @@ const SelectableTimelineComponent: React.FC = ({ windowProps: { onScroll: ({ scrollOffset }) => handleOnScroll( - timelines.filter((t) => !hideUntitled || t.title !== '').length, + (timelines ?? []).filter((t) => !hideUntitled || t.title !== '').length, timelineCount, scrollOffset ), @@ -254,7 +254,7 @@ const SelectableTimelineComponent: React.FC = ({ = ({ searchProps={searchProps} singleSelection={true} options={getSelectableOptions({ - timelines, + timelines: timelines ?? [], onlyFavorites, searchTimelineValue, timelineType, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index b14ccbd319399..82b41a95bd537 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -38,7 +38,7 @@ export interface AllTimelinesArgs { status, timelineType, }: AllTimelinesVariables) => void; - timelines: OpenTimelineResult[]; + timelines: OpenTimelineResult[] | null; loading: boolean; totalCount: number; customTemplateTimelineCount: number; @@ -105,7 +105,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { const [allTimelines, setAllTimelines] = useState>({ loading: false, totalCount: 0, - timelines: [], + timelines: null, // use null as initial state to distinguish between empty result and haven't started loading. customTemplateTimelineCount: 0, defaultTimelineCount: 0, elasticTemplateTimelineCount: 0, @@ -128,7 +128,10 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { const fetchData = async () => { try { if (apolloClient != null) { - setAllTimelines((prevState) => ({ ...prevState, loading: true })); + setAllTimelines((prevState) => ({ + ...prevState, + loading: true, + })); const variables: GetAllTimeline.Variables = { onlyUserFavorite, diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index 53ea28832f47f..806ac57df1f65 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -12,7 +12,6 @@ import { Switch, Route, useHistory } from 'react-router-dom'; import { ChromeBreadcrumb } from '../../../../../../src/core/public'; import { TimelineType } from '../../../common/types/timeline'; -import { TAB_TIMELINES, TAB_TEMPLATES } from '../components/open_timeline/translations'; import { TimelineRouteSpyState } from '../../common/utils/route/types'; import { TimelinesPage } from './timelines_page'; @@ -25,37 +24,18 @@ import { SecurityPageName } from '../../app/types'; const timelinesPagePath = `/:tabName(${TimelineType.default}|${TimelineType.template})`; const timelinesDefaultPath = `/${TimelineType.default}`; -const TabNameMappedToI18nKey: Record = { - [TimelineType.default]: TAB_TIMELINES, - [TimelineType.template]: TAB_TEMPLATES, -}; - export const getBreadcrumbs = ( params: TimelineRouteSpyState, search: string[], getUrlForApp: GetUrlForApp -): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, { - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, - ]; - - const tabName = params?.tabName; - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - return breadcrumb; -}; +): ChromeBreadcrumb[] => [ + { + text: PAGE_TITLE, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), + }, +]; export const Timelines = React.memo(() => { const history = useHistory(); diff --git a/x-pack/plugins/security_solution/public/timelines/pages/translations.ts b/x-pack/plugins/security_solution/public/timelines/pages/translations.ts index f3bff98785619..199fc27c2663a 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/pages/translations.ts @@ -21,7 +21,7 @@ export const ALL_TIMELINES_PANEL_TITLE = i18n.translate( export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.timelines.allTimelines.importTimelineTitle', { - defaultMessage: 'Import Timeline', + defaultMessage: 'Import', } ); diff --git a/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts b/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts index 557b485682e15..a8ed7b77a5d9e 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/kbn_client_with_api_key_support.ts @@ -7,7 +7,7 @@ import { URL } from 'url'; -import { KbnClient, KbnClientOptions } from '@kbn/dev-utils'; +import { KbnClient, KbnClientOptions } from '@kbn/test'; import fetch, { RequestInit } from 'node-fetch'; export class KbnClientWithApiKeySupport extends KbnClient { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index a4664e4a51ca2..72f56f13eaddf 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -10,7 +10,8 @@ import yargs from 'yargs'; import fs from 'fs'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import { KbnClient, ToolingLog, CA_CERT_PATH } from '@kbn/dev-utils'; +import { ToolingLog, CA_CERT_PATH } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts index 3b914f987456a..582969f1dcd45 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/index.ts @@ -7,7 +7,8 @@ // @ts-ignore import minimist from 'minimist'; -import { KbnClient, ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import bluebird from 'bluebird'; import { basename } from 'path'; import { TRUSTED_APPS_CREATE_API, TRUSTED_APPS_LIST_API } from '../../../common/endpoint/constants'; 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 cf6ea572aa856..649ce9ed64365 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 @@ -440,6 +440,35 @@ export const getMlResult = (): RuleAlertType => { }; }; +export const getThresholdResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + type: 'threshold', + threshold: { + field: 'host.ip', + value: 5, + }, + }, + }; +}; + +export const getEqlResult = (): RuleAlertType => { + const result = getResult(); + + return { + ...result, + params: { + ...result.params, + type: 'eql', + query: 'process where true', + }, + }; +}; + export const updateActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', 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 55d128225c555..50823ebd85d05 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 @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { sampleRuleAlertParams, sampleEmptyDocSearchResults, @@ -23,9 +22,10 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mock import uuid from 'uuid'; import { listMock } from '../../../../../lists/server/mocks'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { BulkResponse } from './types'; +import { BulkResponse, RuleRangeTuple } from './types'; import { SearchListItemArraySchema } from '../../../../../lists/common/schemas'; import { getSearchListItemResponseMock } from '../../../../../lists/common/schemas/response/search_list_item_schema.mock'; +import { getRuleRangeTuples } from './utils'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -39,16 +39,26 @@ describe('searchAfterAndBulkCreate', () => { let inputIndexPattern: string[] = []; let listClient = listMock.getListClient(); const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); + const sampleParams = sampleRuleAlertParams(30); + let tuples: RuleRangeTuple[]; beforeEach(() => { jest.clearAllMocks(); listClient = listMock.getListClient(); listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createAlertServices(); + ({ tuples } = getRuleRangeTuples({ + logger: mockLogger, + previousStartedAt: new Date(), + from: sampleParams.from, + to: sampleParams.to, + interval: '5m', + maxSignals: sampleParams.maxSignals, + buildRuleMessage, + })); }); test('should return success with number of searches less than max signals', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -112,11 +122,9 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -147,7 +155,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success with number of searches less than max signals with gap', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -201,8 +208,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: moment.duration(2, 'm'), - previousStartedAt: moment().subtract(10, 'm').toDate(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -233,7 +239,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when no search results are in the allowlist', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -278,8 +283,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -305,7 +309,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(mockService.callCluster).toHaveBeenCalledTimes(3); - expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -316,7 +320,6 @@ describe('searchAfterAndBulkCreate', () => { { ...getSearchListItemResponseMock(), value: ['3.3.3.3'] }, ]; listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce( repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ @@ -342,8 +345,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -382,7 +384,6 @@ describe('searchAfterAndBulkCreate', () => { ]; listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster.mockResolvedValueOnce( repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', @@ -406,8 +407,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -437,13 +437,12 @@ describe('searchAfterAndBulkCreate', () => { expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. - expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[8][0]).toContain( + expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[7][0]).toContain( 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); test('should return success when no sortId present but search results are in the allowlist', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -487,8 +486,7 @@ describe('searchAfterAndBulkCreate', () => { ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [exceptionItem], services: mockService, @@ -514,17 +512,16 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(mockService.callCluster).toHaveBeenCalledTimes(2); - expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); // I don't like testing log statements since logs change but this is the best // way I can think of to ensure this section is getting hit with this test case. - expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[15][0]).toContain( + expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[14][0]).toContain( 'ran out of sort ids to sort on name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' ); }); test('should return success when no exceptions list provided', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -565,8 +562,7 @@ describe('searchAfterAndBulkCreate', () => { ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [], services: mockService, @@ -592,7 +588,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(mockService.callCluster).toHaveBeenCalledTimes(3); - expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -609,15 +605,13 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - 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: [exceptionItem], - gap: null, - previousStartedAt: new Date(), + tuples, ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -659,7 +653,6 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( @@ -672,8 +665,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, exceptionsList: [exceptionItem], - gap: null, - previousStartedAt: new Date(), + tuples, ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -702,7 +694,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('if returns false when singleSearchAfter throws an exception', async () => { - const sampleParams = sampleRuleAlertParams(10); mockService.callCluster .mockResolvedValueOnce({ took: 100, @@ -741,8 +732,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, exceptionsList: [exceptionItem], - gap: null, - previousStartedAt: new Date(), + tuples, ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -771,7 +761,6 @@ describe('searchAfterAndBulkCreate', () => { }); test('it returns error array when singleSearchAfter returns errors', async () => { - const sampleParams = sampleRuleAlertParams(30); const bulkItem: BulkResponse = { took: 100, errors: true, @@ -832,7 +821,6 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); - const { success, createdSignalsCount, @@ -840,8 +828,7 @@ describe('searchAfterAndBulkCreate', () => { errors, } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, - gap: null, - previousStartedAt: new Date(), + tuples, listClient, exceptionsList: [], services: mockService, @@ -873,7 +860,6 @@ describe('searchAfterAndBulkCreate', () => { }); it('invokes the enrichment callback with signal search results', async () => { - const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockResolvedValueOnce({ @@ -917,8 +903,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ enrichment: mockEnrichment, ruleParams: sampleParams, - gap: moment.duration(2, 'm'), - previousStartedAt: moment().subtract(10, 'm').toDate(), + tuples, listClient, exceptionsList: [], services: mockService, 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 061aa4bba5a41..1dd3a2d2173a8 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 @@ -17,7 +17,6 @@ import { createSearchResultReturnType, createSearchAfterReturnTypeFromResponse, createTotalHitsFromSearchResult, - getSignalTimeTuples, mergeReturns, mergeSearchResults, } from './utils'; @@ -25,8 +24,7 @@ import { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } fr // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ - gap, - previousStartedAt, + tuples: totalToFromTuples, ruleParams, exceptionsList, services, @@ -64,16 +62,6 @@ export const searchAfterAndBulkCreate = async ({ // to ensure we don't exceed maxSignals let signalsCreatedCount = 0; - const totalToFromTuples = getSignalTimeTuples({ - logger, - ruleParamsFrom: ruleParams.from, - ruleParamsTo: ruleParams.to, - ruleParamsMaxSignals: ruleParams.maxSignals, - gap, - previousStartedAt, - interval, - buildRuleMessage, - }); const tuplesToBeLogged = [...totalToFromTuples]; logger.debug(buildRuleMessage(`totalToFromTuples: ${totalToFromTuples.length}`)); 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 a79961eb716fd..d3d82682cbb4a 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 @@ -7,18 +7,16 @@ import moment from 'moment'; import { loggingSystemMock } from 'src/core/server/mocks'; -import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; +import { + getResult, + getMlResult, + getThresholdResult, + getEqlResult, +} 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, - getGapMaxCatchupRatio, - getListsClient, - getExceptions, - sortExceptionItems, - checkPrivileges, -} from './utils'; +import { getListsClient, getExceptions, sortExceptionItems, checkPrivileges } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; @@ -31,6 +29,7 @@ import { getListClientMock } from '../../../../../lists/server/services/lists/li import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; +import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -40,8 +39,6 @@ jest.mock('./utils', () => { const original = jest.requireActual('./utils'); return { ...original, - getGapBetweenRuns: jest.fn(), - getGapMaxCatchupRatio: jest.fn(), getListsClient: jest.fn(), getExceptions: jest.fn(), sortExceptionItems: jest.fn(), @@ -113,7 +110,6 @@ describe('rules_notification_alert_type', () => { warning: jest.fn(), }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); (getListsClient as jest.Mock).mockReturnValue({ listClient: getListClientMock(), exceptionsClient: getExceptionListClientMock(), @@ -124,7 +120,6 @@ describe('rules_notification_alert_type', () => { exceptionsWithValueLists: [], }); (searchAfterAndBulkCreate as jest.Mock).mockClear(); - (getGapMaxCatchupRatio as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, searchAfterTimes: [], @@ -187,23 +182,12 @@ describe('rules_notification_alert_type', () => { describe('executor', () => { it('should warn about the gap between runs if gap is very large', async () => { - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(100, 'm')); - (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ - maxCatchup: 4, - ratio: 20, - gapDiffInUnits: 95, - }); + payload.previousStartedAt = moment().subtract(100, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); - expect(logger.warn.mock.calls[0][0]).toContain( - '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' - ); expect(ruleStatusService.error).toHaveBeenCalled(); - expect(ruleStatusService.error.mock.calls[0][0]).toContain( - '2 hours (6000000ms) has passed since last rule execution, and signals may have been missed.' - ); expect(ruleStatusService.error.mock.calls[0][1]).toEqual({ - gap: '2 hours', + gap: 'an hour', }); }); @@ -233,6 +217,30 @@ describe('rules_notification_alert_type', () => { ); }); + it('should set a warning when exception list for threshold rule contains value list exceptions', async () => { + (getExceptions as jest.Mock).mockReturnValue([ + getExceptionListItemSchemaMock({ entries: [getEntryListMock()] }), + ]); + payload = getPayload(getThresholdResult(), alertServices); + await alert.executor(payload); + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' + ); + }); + + it('should set a warning when exception list for EQL rule contains value list exceptions', async () => { + (getExceptions as jest.Mock).mockReturnValue([ + getExceptionListItemSchemaMock({ entries: [getEntryListMock()] }), + ]); + payload = getPayload(getEqlResult(), alertServices); + await alert.executor(payload); + expect(ruleStatusService.warning).toHaveBeenCalled(); + expect(ruleStatusService.warning.mock.calls[0][0]).toContain( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' + ); + }); + it('should set a failure status for when rules cannot read ANY provided indices', async () => { (checkPrivileges as jest.Mock).mockResolvedValueOnce({ username: 'elastic', @@ -257,12 +265,7 @@ describe('rules_notification_alert_type', () => { }); it('should NOT warn about the gap between runs if gap small', async () => { - (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1, 'm')); - (getGapMaxCatchupRatio as jest.Mock).mockReturnValue({ - maxCatchup: 1, - ratio: 1, - gapDiffInUnits: 1, - }); + payload.previousStartedAt = moment().subtract(10, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalledTimes(0); expect(ruleStatusService.error).toHaveBeenCalledTimes(0); @@ -450,6 +453,7 @@ describe('rules_notification_alert_type', () => { const ruleAlert = getMlResult(); ruleAlert.params.anomalyThreshold = undefined; payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain( @@ -460,6 +464,7 @@ describe('rules_notification_alert_type', () => { it('should throw an error if Machine learning job summary was null', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([]); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); @@ -473,6 +478,7 @@ describe('rules_notification_alert_type', () => { it('should log an error if Machine learning job was not started', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -518,6 +524,7 @@ describe('rules_notification_alert_type', () => { it('should call ruleStatusService.success if signals were created', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', @@ -544,6 +551,7 @@ describe('rules_notification_alert_type', () => { it('should not call checkPrivileges if ML rule', async () => { const ruleAlert = getMlResult(); payload = getPayload(ruleAlert, alertServices) as jest.Mocked; + payload.previousStartedAt = null; jobsSummaryMock.mockResolvedValue([ { id: 'some_job_id', 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 98c9dd41d179c..14a65bc1eeb7b 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 @@ -24,6 +24,7 @@ import { isThresholdRule, isEqlRule, isThreatMatchRule, + hasLargeValueItem, } from '../../../../common/detection_engine/utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; @@ -37,11 +38,8 @@ import { WrappedSignalHit, } from './types'; import { - getGapBetweenRuns, getListsClient, getExceptions, - getGapMaxCatchupRatio, - MAX_RULE_GAP_RATIO, wrapSignal, createErrorsFromShard, createSearchAfterReturnType, @@ -50,6 +48,7 @@ import { checkPrivileges, hasTimestampFields, hasReadIndexPrivileges, + getRuleRangeTuples, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; @@ -230,29 +229,24 @@ export const signalRulesAlertType = ({ } catch (exc) { logger.error(buildRuleMessage(`Check privileges failed to execute ${exc}`)); } - - const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); - if (gap != null && gap.asMilliseconds() > 0) { - const fromUnit = from[from.length - 1]; - const { ratio } = getGapMaxCatchupRatio({ - logger, - buildRuleMessage, - previousStartedAt, - ruleParamsFrom: from, - interval, - unit: fromUnit, - }); - if (ratio && ratio >= MAX_RULE_GAP_RATIO) { - const gapString = gap.humanize(); - const gapMessage = buildRuleMessage( - `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, - 'Consider increasing your look behind time or adding more Kibana instances.' - ); - logger.warn(gapMessage); - - hasError = true; - await ruleStatusService.error(gapMessage, { gap: gapString }); - } + const { tuples, remainingGap } = getRuleRangeTuples({ + logger, + previousStartedAt, + from, + to, + interval, + maxSignals, + buildRuleMessage, + }); + if (remainingGap.asMilliseconds() > 0) { + const gapString = remainingGap.humanize(); + const gapMessage = buildRuleMessage( + `${gapString} (${remainingGap.asMilliseconds()}ms) were not queried between this rule execution and the last execution, so signals may have been missed.`, + 'Consider increasing your look behind time or adding more Kibana instances.' + ); + logger.warn(gapMessage); + hasError = true; + await ruleStatusService.error(gapMessage, { gap: gapString }); } try { const { listClient, exceptionsClient } = getListsClient({ @@ -372,6 +366,12 @@ export const signalRulesAlertType = ({ }), ]); } else if (isThresholdRule(type) && threshold) { + if (hasLargeValueItem(exceptionItems ?? [])) { + await ruleStatusService.warning( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to Threshold rules' + ); + wroteWarningStatus = true; + } const inputIndex = await getInputIndex(services, version, index); const thresholdFields = Array.isArray(threshold.field) @@ -479,6 +479,7 @@ export const signalRulesAlertType = ({ } const inputIndex = await getInputIndex(services, version, index); result = await createThreatSignals({ + tuples, threatMapping, query, inputIndex, @@ -489,8 +490,6 @@ export const signalRulesAlertType = ({ savedId, services, exceptionItems: exceptionItems ?? [], - gap, - previousStartedAt, listClient, logger, eventsTelemetry, @@ -531,8 +530,7 @@ export const signalRulesAlertType = ({ }); result = await searchAfterAndBulkCreate({ - gap, - previousStartedAt, + tuples, listClient, exceptionsList: exceptionItems ?? [], ruleParams: params, @@ -561,6 +559,12 @@ export const signalRulesAlertType = ({ if (query === undefined) { throw new Error('EQL query rule must have a query defined'); } + if (hasLargeValueItem(exceptionItems ?? [])) { + await ruleStatusService.warning( + 'Exceptions that use "is in list" or "is not in list" operators are not applied to EQL rules' + ); + wroteWarningStatus = true; + } try { const signalIndexVersion = await getIndexVersion(services.callCluster, outputIndex); if (isOutdated({ current: signalIndexVersion, target: MIN_EQL_RULE_INDEX_VERSION })) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index ba428bc077125..d9c72f7f95679 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -13,6 +13,7 @@ import { CreateThreatSignalOptions } from './types'; import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ + tuples, threatMapping, threatEnrichment, query, @@ -23,8 +24,6 @@ export const createThreatSignal = async ({ savedId, services, exceptionItems, - gap, - previousStartedAt, listClient, logger, eventsTelemetry, @@ -80,8 +79,7 @@ export const createThreatSignal = async ({ ); const result = await searchAfterAndBulkCreate({ - gap, - previousStartedAt, + tuples, listClient, exceptionsList: exceptionItems, ruleParams: params, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index e45aea29c423f..854c2b8f3fdc1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -15,6 +15,7 @@ import { combineConcurrentResults } from './utils'; import { buildThreatEnrichment } from './build_threat_enrichment'; export const createThreatSignals = async ({ + tuples, threatMapping, query, inputIndex, @@ -24,8 +25,6 @@ export const createThreatSignals = async ({ savedId, services, exceptionItems, - gap, - previousStartedAt, listClient, logger, eventsTelemetry, @@ -111,6 +110,7 @@ export const createThreatSignals = async ({ const concurrentSearchesPerformed = chunks.map>( (slicedChunk) => createThreatSignal({ + tuples, threatEnrichment, threatMapping, query, @@ -121,8 +121,6 @@ export const createThreatSignals = async ({ savedId, services, exceptionItems, - gap, - previousStartedAt, listClient, logger, eventsTelemetry, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index a022cbbdd4042..1c35a5af09b38 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -6,7 +6,6 @@ */ import { SearchResponse } from 'elasticsearch'; -import { Duration } from 'moment'; import { ListClient } from '../../../../../../lists/server'; import { @@ -34,11 +33,12 @@ import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/ import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; -import { SearchAfterAndBulkCreateReturnType, SignalsEnrichment } from '../types'; +import { RuleRangeTuple, SearchAfterAndBulkCreateReturnType, SignalsEnrichment } from '../types'; export type SortOrderOrUndefined = 'asc' | 'desc' | undefined; export interface CreateThreatSignalsOptions { + tuples: RuleRangeTuple[]; threatMapping: ThreatMapping; query: string; inputIndex: string[]; @@ -48,8 +48,6 @@ export interface CreateThreatSignalsOptions { savedId: string | undefined; services: AlertServices; exceptionItems: ExceptionListItemSchema[]; - gap: Duration | null; - previousStartedAt: Date | null; listClient: ListClient; logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; @@ -79,6 +77,7 @@ export interface CreateThreatSignalsOptions { } export interface CreateThreatSignalOptions { + tuples: RuleRangeTuple[]; threatMapping: ThreatMapping; threatEnrichment: SignalsEnrichment; query: string; @@ -89,8 +88,6 @@ export interface CreateThreatSignalOptions { savedId: string | undefined; services: AlertServices; exceptionItems: ExceptionListItemSchema[]; - gap: Duration | null; - previousStartedAt: Date | null; listClient: ListClient; logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index e5ca1f6a60456..f759da31566e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -73,6 +73,12 @@ export interface ThresholdSignalHistory { [hash: string]: ThresholdSignalHistoryRecord; } +export interface RuleRangeTuple { + to: moment.Moment; + from: moment.Moment; + maxSignals: number; +} + export interface SignalSource { [key: string]: SearchTypes; // TODO: SignalSource is being used as the type for documents matching detection engine queries, but they may not @@ -251,8 +257,11 @@ export interface QueryFilter { export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise; export interface SearchAfterAndBulkCreateParams { - gap: moment.Duration | null; - previousStartedAt: Date | null | undefined; + tuples: Array<{ + to: moment.Moment; + from: moment.Moment; + maxSignals: number; + }>; ruleParams: RuleTypeParams; services: AlertServices; listClient: ListClient; 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 7888bb6deaab7..0a581816ee82f 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 @@ -25,10 +25,10 @@ import { parseInterval, getDriftTolerance, getGapBetweenRuns, - getGapMaxCatchupRatio, + getNumCatchupIntervals, errorAggregator, getListsClient, - getSignalTimeTuples, + getRuleRangeTuples, getExceptions, hasTimestampFields, wrapBuildingBlocks, @@ -109,7 +109,7 @@ describe('utils', () => { expect(duration?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); }); - test('it returns null given an invalid duration', () => { + test('it throws given an invalid duration', () => { const duration = parseInterval('junk'); expect(duration).toBeNull(); }); @@ -148,7 +148,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-6m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -158,7 +158,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-5m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift?.asMilliseconds()).toEqual(0); }); @@ -167,7 +167,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); @@ -177,7 +177,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', - interval: moment.duration(0, 'milliseconds'), + intervalDuration: moment.duration(0, 'milliseconds'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); @@ -187,7 +187,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'invalid', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -197,7 +197,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: '10m', to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -207,7 +207,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now-1m', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); @@ -217,7 +217,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: moment().subtract(10, 'minutes').toISOString(), to: 'now', - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); @@ -227,7 +227,7 @@ describe('utils', () => { const drift = getDriftTolerance({ from: 'now-6m', to: moment().toISOString(), - interval: moment.duration(5, 'minutes'), + intervalDuration: moment.duration(5, 'minutes'), }); expect(drift).not.toBeNull(); expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); @@ -238,7 +238,7 @@ describe('utils', () => { test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-5m', to: 'now', now: nowDate.clone(), @@ -250,7 +250,7 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -262,7 +262,7 @@ describe('utils', () => { test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-10m', to: 'now', now: nowDate.clone(), @@ -274,7 +274,7 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(10, 'minutes').toDate(), - interval: '10m', + intervalDuration: moment.duration(10, 'minutes'), from: 'now-11m', to: 'now', now: nowDate.clone(), @@ -286,7 +286,7 @@ describe('utils', () => { test('it returns a gap of only -30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is 30 seconds more', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes').subtract(30, 'seconds').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -298,7 +298,7 @@ describe('utils', () => { test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(6, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -310,7 +310,7 @@ describe('utils', () => { test('it returns a gap of 30 seconds when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute and 30 seconds late', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(6, 'minutes').subtract(30, 'seconds').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -322,7 +322,7 @@ describe('utils', () => { test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(7, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'now', now: nowDate.clone(), @@ -331,32 +331,21 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns null if given a previousStartedAt of null', () => { + test('it returns 0 if given a previousStartedAt of null', () => { const gap = getGapBetweenRuns({ previousStartedAt: null, - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-5m', to: 'now', now: nowDate.clone(), }); - expect(gap).toBeNull(); - }); - - test('it returns null if the interval is an invalid string such as "invalid"', () => { - const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().toDate(), - interval: 'invalid', // if not set to "x" where x is an interval such as 6m - from: 'now-5m', - to: 'now', - now: nowDate.clone(), - }); - expect(gap).toBeNull(); + expect(gap.asMilliseconds()).toEqual(0); }); test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(7, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'invalid', to: 'now', now: nowDate.clone(), @@ -368,7 +357,7 @@ describe('utils', () => { test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(7, 'minutes').toDate(), - interval: '5m', + intervalDuration: moment.duration(5, 'minutes'), from: 'now-6m', to: 'invalid', now: nowDate.clone(), @@ -609,134 +598,116 @@ describe('utils', () => { }); }); - describe('getSignalTimeTuples', () => { + describe('getRuleRangeTuples', () => { test('should return a single tuple if no gap', () => { - const someTuples = getSignalTimeTuples({ + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: null, previousStartedAt: moment().subtract(30, 's').toDate(), interval: '30s', - ruleParamsFrom: 'now-30s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-30s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - const someTuple = someTuples[0]; + const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); + expect(tuples.length).toEqual(1); + expect(remainingGap.asMilliseconds()).toEqual(0); + }); + + test('should return a single tuple if malformed interval prevents gap calculation', () => { + const { tuples, remainingGap } = getRuleRangeTuples({ + logger: mockLogger, + previousStartedAt: moment().subtract(30, 's').toDate(), + interval: 'invalid', + from: 'now-30s', + to: 'now', + maxSignals: 20, + buildRuleMessage, + }); + const someTuple = tuples[0]; + expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); + expect(tuples.length).toEqual(1); + expect(remainingGap.asMilliseconds()).toEqual(0); }); test('should return two tuples if gap and previouslyStartedAt', () => { - const someTuples = getSignalTimeTuples({ + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: moment.duration(10, 's'), previousStartedAt: moment().subtract(65, 's').toDate(), interval: '50s', - ruleParamsFrom: 'now-55s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-55s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - const someTuple = someTuples[1]; - expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(10); + const someTuple = tuples[1]; + expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(55); + expect(remainingGap.asMilliseconds()).toEqual(0); }); test('should return five tuples when give long gap', () => { - const someTuples = getSignalTimeTuples({ + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: moment.duration(65, 's'), // 64 is 5 times the interval + lookback, which will trigger max lookback - previousStartedAt: moment().subtract(65, 's').toDate(), + previousStartedAt: moment().subtract(65, 's').toDate(), // 64 is 5 times the interval + lookback, which will trigger max lookback interval: '10s', - ruleParamsFrom: 'now-13s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-13s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - expect(someTuples.length).toEqual(5); - someTuples.forEach((item, index) => { + expect(tuples.length).toEqual(5); + tuples.forEach((item, index) => { if (index === 0) { return; } - expect(moment(item.to).diff(moment(item.from), 's')).toEqual(10); + expect(moment(item.to).diff(moment(item.from), 's')).toEqual(13); + expect(item.to.diff(tuples[index - 1].to, 's')).toEqual(-10); + expect(item.from.diff(tuples[index - 1].from, 's')).toEqual(-10); }); + expect(remainingGap.asMilliseconds()).toEqual(12000); }); - // this tests if calculatedFrom in utils.ts:320 parses an int and not a float - // if we don't parse as an int, then dateMath.parse will fail - // as it doesn't support parsing `now-67.549`, it only supports ints like `now-67`. - test('should return five tuples when given a gap with a decimal to ensure no parsing errors', () => { - const someTuples = getSignalTimeTuples({ + test('should return a single tuple when give a negative gap (rule ran sooner than expected)', () => { + const { tuples, remainingGap } = getRuleRangeTuples({ logger: mockLogger, - gap: moment.duration(67549, 'ms'), // 64 is 5 times the interval + lookback, which will trigger max lookback - previousStartedAt: moment().subtract(67549, 'ms').toDate(), - interval: '10s', - ruleParamsFrom: 'now-13s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, - buildRuleMessage, - }); - expect(someTuples.length).toEqual(5); - }); - - test('should return single tuples when give a negative gap (rule ran sooner than expected)', () => { - const someTuples = getSignalTimeTuples({ - logger: mockLogger, - gap: moment.duration(-15, 's'), // 64 is 5 times the interval + lookback, which will trigger max lookback previousStartedAt: moment().subtract(-15, 's').toDate(), interval: '10s', - ruleParamsFrom: 'now-13s', - ruleParamsTo: 'now', - ruleParamsMaxSignals: 20, + from: 'now-13s', + to: 'now', + maxSignals: 20, buildRuleMessage, }); - expect(someTuples.length).toEqual(1); - const someTuple = someTuples[0]; + expect(tuples.length).toEqual(1); + const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(13); + expect(remainingGap.asMilliseconds()).toEqual(0); }); }); describe('getMaxCatchupRatio', () => { - test('should return null if rule has never run before', () => { - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger: mockLogger, - previousStartedAt: null, - interval: '30s', - ruleParamsFrom: 'now-30s', - buildRuleMessage, - unit: 's', + test('should return 0 if gap is 0', () => { + const catchup = getNumCatchupIntervals({ + gap: moment.duration(0), + intervalDuration: moment.duration(11000), }); - expect(maxCatchup).toBeNull(); - expect(ratio).toBeNull(); - expect(gapDiffInUnits).toBeNull(); + expect(catchup).toEqual(0); }); - test('should should have non-null values when gap is present', () => { - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger: mockLogger, - previousStartedAt: moment().subtract(65, 's').toDate(), - interval: '50s', - ruleParamsFrom: 'now-55s', - buildRuleMessage, - unit: 's', + test('should return 1 if gap is in (0, intervalDuration]', () => { + const catchup = getNumCatchupIntervals({ + gap: moment.duration(10000), + intervalDuration: moment.duration(10000), }); - expect(maxCatchup).toEqual(0.2); - expect(ratio).toEqual(0.2); - expect(gapDiffInUnits).toEqual(10); + expect(catchup).toEqual(1); }); - // when a rule runs sooner than expected we don't - // consider that a gap as that is a very rare circumstance - test('should return null when given a negative gap (rule ran sooner than expected)', () => { - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger: mockLogger, - previousStartedAt: moment().subtract(-15, 's').toDate(), - interval: '10s', - ruleParamsFrom: 'now-13s', - buildRuleMessage, - unit: 's', + test('should round up return value', () => { + const catchup = getNumCatchupIntervals({ + gap: moment.duration(15000), + intervalDuration: moment.duration(11000), }); - expect(maxCatchup).toBeNull(); - expect(ratio).toBeNull(); - expect(gapDiffInUnits).toBeNull(); + expect(catchup).toEqual(2); }); }); 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 58bf22be97bf8..2b306cd2a8d9d 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 @@ -29,12 +29,12 @@ import { ListArray } from '../../../../common/detection_engine/schemas/types/lis import { BulkResponse, BulkResponseErrorAggregation, - isValidUnit, SignalHit, SearchAfterAndBulkCreateReturnType, SignalSearchResponse, Signal, WrappedSignalHit, + RuleRangeTuple, } from './types'; import { BuildRuleMessage } from './rule_messages'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; @@ -163,82 +163,21 @@ export const checkPrivileges = async ( }, }); -export const getGapMaxCatchupRatio = ({ - logger, - previousStartedAt, - unit, - buildRuleMessage, - ruleParamsFrom, - interval, +export const getNumCatchupIntervals = ({ + gap, + intervalDuration, }: { - logger: Logger; - ruleParamsFrom: string; - previousStartedAt: Date | null | undefined; - interval: string; - buildRuleMessage: BuildRuleMessage; - unit: string; -}): { - maxCatchup: number | null; - ratio: number | null; - gapDiffInUnits: number | null; -} => { - if (previousStartedAt == null) { - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; - } - if (!isValidUnit(unit)) { - logger.error(buildRuleMessage(`unit: ${unit} failed isValidUnit check`)); - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; - } - /* - we need the total duration from now until the last time the rule ran. - the next few lines can be summed up as calculating - "how many second | minutes | hours have passed since the last time this ran?" - */ - const nowToGapDiff = moment.duration(moment().diff(previousStartedAt)); - // rule ran early, no gap - if (shorthandMap[unit].asFn(nowToGapDiff) < 0) { - // rule ran early, no gap - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; - } - const calculatedFrom = `now-${ - parseInt(shorthandMap[unit].asFn(nowToGapDiff).toString(), 10) + unit - }`; - logger.debug(buildRuleMessage(`calculatedFrom: ${calculatedFrom}`)); - - const intervalMoment = moment.duration(parseInt(interval, 10), unit); - logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); - const calculatedFromAsMoment = dateMath.parse(calculatedFrom); - const dateMathRuleParamsFrom = dateMath.parse(ruleParamsFrom); - if (dateMathRuleParamsFrom != null && intervalMoment != null) { - const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; - const gapDiffInUnits = dateMathRuleParamsFrom.diff(calculatedFromAsMoment, momentUnit); - - const ratio = gapDiffInUnits / shorthandMap[unit].asFn(intervalMoment); - - // maxCatchup is to ensure we are not trying to catch up too far back. - // This allows for a maximum of 4 consecutive rule execution misses - // to be included in the number of signals generated. - const maxCatchup = ratio < MAX_RULE_GAP_RATIO ? ratio : MAX_RULE_GAP_RATIO; - return { maxCatchup, ratio, gapDiffInUnits }; + gap: moment.Duration; + intervalDuration: moment.Duration; +}): number => { + if (gap.asMilliseconds() <= 0 || intervalDuration.asMilliseconds() <= 0) { + return 0; } - logger.error(buildRuleMessage('failed to parse calculatedFrom and intervalMoment')); - return { - maxCatchup: null, - ratio: null, - gapDiffInUnits: null, - }; + const ratio = Math.ceil(gap.asMilliseconds() / intervalDuration.asMilliseconds()); + // maxCatchup is to ensure we are not trying to catch up too far back. + // This allows for a maximum of 4 consecutive rule execution misses + // to be included in the number of signals generated. + return ratio < MAX_RULE_GAP_RATIO ? ratio : MAX_RULE_GAP_RATIO; }; export const getListsClient = ({ @@ -396,50 +335,40 @@ export const parseInterval = (intervalString: string): moment.Duration | null => export const getDriftTolerance = ({ from, to, - interval, + intervalDuration, now = moment(), }: { from: string; to: string; - interval: moment.Duration; + intervalDuration: moment.Duration; now?: moment.Moment; -}): moment.Duration | null => { +}): moment.Duration => { const toDate = parseScheduleDates(to) ?? now; const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); const timeSegment = toDate.diff(fromDate); const duration = moment.duration(timeSegment); - if (duration !== null) { - return duration.subtract(interval); - } else { - return null; - } + return duration.subtract(intervalDuration); }; export const getGapBetweenRuns = ({ previousStartedAt, - interval, + intervalDuration, from, to, now = moment(), }: { previousStartedAt: Date | undefined | null; - interval: string; + intervalDuration: moment.Duration; from: string; to: string; now?: moment.Moment; -}): moment.Duration | null => { +}): moment.Duration => { if (previousStartedAt == null) { - return null; - } - const intervalDuration = parseInterval(interval); - if (intervalDuration == null) { - return null; - } - const driftTolerance = getDriftTolerance({ from, to, interval: intervalDuration }); - if (driftTolerance == null) { - return null; + return moment.duration(0); } + const driftTolerance = getDriftTolerance({ from, to, intervalDuration }); + const diff = moment.duration(now.diff(previousStartedAt)); const drift = diff.subtract(intervalDuration); return drift.subtract(driftTolerance); @@ -489,135 +418,103 @@ export const errorAggregator = ( }, Object.create(null)); }; -/** - * Determines the number of time intervals to search if gap is present - * along with new maxSignals per time interval. - * @param logger Logger - * @param ruleParamsFrom string representing the rules 'from' property - * @param ruleParamsTo string representing the rules 'to' property - * @param ruleParamsMaxSignals int representing the maxSignals property on the rule (usually unmodified at 100) - * @param gap moment.Duration representing a gap in since the last time the rule ran - * @param previousStartedAt Date at which the rule last ran - * @param interval string the interval which the rule runs - * @param buildRuleMessage function provides meta information for logged event - */ -export const getSignalTimeTuples = ({ +export const getRuleRangeTuples = ({ logger, - ruleParamsFrom, - ruleParamsTo, - ruleParamsMaxSignals, - gap, previousStartedAt, + from, + to, interval, + maxSignals, buildRuleMessage, }: { logger: Logger; - ruleParamsFrom: string; - ruleParamsTo: string; - ruleParamsMaxSignals: number; - gap: moment.Duration | null; previousStartedAt: Date | null | undefined; + from: string; + to: string; interval: string; - buildRuleMessage: BuildRuleMessage; -}): Array<{ - to: moment.Moment | undefined; - from: moment.Moment | undefined; maxSignals: number; -}> => { - let totalToFromTuples: Array<{ - to: moment.Moment | undefined; - from: moment.Moment | undefined; - maxSignals: number; - }> = []; - if (gap != null && gap.valueOf() > 0 && previousStartedAt != null) { - const fromUnit = ruleParamsFrom[ruleParamsFrom.length - 1]; - if (isValidUnit(fromUnit)) { - const unit = fromUnit; // only seconds (s), minutes (m) or hours (h) - - /* - we need the total duration from now until the last time the rule ran. - the next few lines can be summed up as calculating - "how many second | minutes | hours have passed since the last time this ran?" - */ - const nowToGapDiff = moment.duration(moment().diff(previousStartedAt)); - const calculatedFrom = `now-${ - parseInt(shorthandMap[unit].asFn(nowToGapDiff).toString(), 10) + unit - }`; - logger.debug(buildRuleMessage(`calculatedFrom: ${calculatedFrom}`)); - - const intervalMoment = moment.duration(parseInt(interval, 10), unit); - logger.debug(buildRuleMessage(`intervalMoment: ${shorthandMap[unit].asFn(intervalMoment)}`)); - const momentUnit = shorthandMap[unit].momentString as moment.DurationInputArg2; - // maxCatchup is to ensure we are not trying to catch up too far back. - // This allows for a maximum of 4 consecutive rule execution misses - // to be included in the number of signals generated. - const { maxCatchup, ratio, gapDiffInUnits } = getGapMaxCatchupRatio({ - logger, - buildRuleMessage, - previousStartedAt, - unit, - ruleParamsFrom, - interval, - }); - logger.debug(buildRuleMessage(`maxCatchup: ${maxCatchup}, ratio: ${ratio}`)); - if (maxCatchup == null || ratio == null || gapDiffInUnits == null) { - throw new Error( - buildRuleMessage('failed to calculate maxCatchup, ratio, or gapDiffInUnits') - ); - } - let tempTo = dateMath.parse(ruleParamsFrom); - if (tempTo == null) { - // return an error - throw new Error(buildRuleMessage('dateMath parse failed')); - } - - let beforeMutatedFrom: moment.Moment | undefined; - while (totalToFromTuples.length < maxCatchup) { - // if maxCatchup is less than 1, we calculate the 'from' differently - // and maxSignals becomes some less amount of maxSignals - // in order to maintain maxSignals per full rule interval. - if (maxCatchup > 0 && maxCatchup < 1) { - totalToFromTuples.push({ - to: tempTo.clone(), - from: tempTo.clone().subtract(gapDiffInUnits, momentUnit), - maxSignals: ruleParamsMaxSignals * maxCatchup, - }); - break; - } - const beforeMutatedTo = tempTo.clone(); - - // moment.subtract mutates the moment so we need to clone again.. - beforeMutatedFrom = tempTo.clone().subtract(intervalMoment, momentUnit); - const tuple = { - to: beforeMutatedTo, - from: beforeMutatedFrom, - maxSignals: ruleParamsMaxSignals, - }; - totalToFromTuples = [...totalToFromTuples, tuple]; - tempTo = beforeMutatedFrom; - } - totalToFromTuples = [ - { - to: dateMath.parse(ruleParamsTo), - from: dateMath.parse(ruleParamsFrom), - maxSignals: ruleParamsMaxSignals, - }, - ...totalToFromTuples, - ]; - } - } else { - totalToFromTuples = [ - { - to: dateMath.parse(ruleParamsTo), - from: dateMath.parse(ruleParamsFrom), - maxSignals: ruleParamsMaxSignals, - }, - ]; + buildRuleMessage: BuildRuleMessage; +}) => { + const originalTo = dateMath.parse(to); + const originalFrom = dateMath.parse(from); + if (originalTo == null || originalFrom == null) { + throw new Error(buildRuleMessage('dateMath parse failed')); + } + const tuples = [ + { + to: originalTo, + from: originalFrom, + maxSignals, + }, + ]; + const intervalDuration = parseInterval(interval); + if (intervalDuration == null) { + logger.error(`Failed to compute gap between rule runs: could not parse rule interval`); + return { tuples, remainingGap: moment.duration(0) }; } - logger.debug( - buildRuleMessage(`totalToFromTuples: ${JSON.stringify(totalToFromTuples, null, 4)}`) + const gap = getGapBetweenRuns({ previousStartedAt, intervalDuration, from, to }); + const catchup = getNumCatchupIntervals({ + gap, + intervalDuration, + }); + const catchupTuples = getCatchupTuples({ + to: originalTo, + from: originalFrom, + ruleParamsMaxSignals: maxSignals, + catchup, + intervalDuration, + }); + tuples.push(...catchupTuples); + // Each extra tuple adds one extra intervalDuration to the time range this rule will cover. + const remainingGapMilliseconds = Math.max( + gap.asMilliseconds() - catchup * intervalDuration.asMilliseconds(), + 0 ); - return totalToFromTuples; + return { tuples, remainingGap: moment.duration(remainingGapMilliseconds) }; +}; + +/** + * Creates rule range tuples needed to cover gaps since the last rule run. + * @param to moment.Moment representing the rules 'to' property + * @param from moment.Moment representing the rules 'from' property + * @param ruleParamsMaxSignals int representing the maxSignals property on the rule (usually unmodified at 100) + * @param catchup number the number of additional rule run intervals to add + * @param intervalDuration moment.Duration the interval which the rule runs + */ +export const getCatchupTuples = ({ + to, + from, + ruleParamsMaxSignals, + catchup, + intervalDuration, +}: { + to: moment.Moment; + from: moment.Moment; + ruleParamsMaxSignals: number; + catchup: number; + intervalDuration: moment.Duration; +}): RuleRangeTuple[] => { + const catchupTuples: RuleRangeTuple[] = []; + const intervalInMilliseconds = intervalDuration.asMilliseconds(); + let currentTo = to; + let currentFrom = from; + // This loop will create tuples with overlapping time ranges, the same way rule runs have overlapping time + // ranges due to the additional lookback. We could choose to create tuples that don't overlap here by using the + // "from" value from one tuple as "to" in the next one, however, the overlap matters for rule types like EQL and + // threshold rules that look for sets of documents within the query. Thus we keep the overlap so that these + // extra tuples behave as similarly to the regular rule runs as possible. + while (catchupTuples.length < catchup) { + const nextTo = currentTo.clone().subtract(intervalInMilliseconds); + const nextFrom = currentFrom.clone().subtract(intervalInMilliseconds); + catchupTuples.push({ + to: nextTo, + from: nextFrom, + maxSignals: ruleParamsMaxSignals, + }); + currentTo = nextTo; + currentFrom = nextFrom; + } + return catchupTuples; }; /** diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index e153c6d42225f..6ce42eabeca5e 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -364,74 +364,75 @@ const allowlistEventFields: AllowlistFields = { pid: true, ppid: true, }, - Target: { - process: { - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, + token: { + integrity_level_name: true, + }, + thread: true, + }, + Target: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, }, - parent: { - process: { - Ext: { - architecture: true, - code_signature: true, - dll: true, - token: { - integrity_level_name: true, - }, + }, + parent: { + process: { + Ext: { + architecture: true, + code_signature: true, + dll: true, + token: { + integrity_level_name: true, }, }, }, - thread: { - Ext: { - call_stack: true, - start_address: true, - start_address_details: { - address_offset: true, - allocation_base: true, - allocation_protection: true, - allocation_size: true, - allocation_type: true, - base_address: true, - bytes_start_address: true, - compressed_bytes: true, - dest_bytes: true, - dest_bytes_disasm: true, - dest_bytes_disasm_hash: true, - pe: { - Ext: { - legal_copyright: true, - product_version: true, - code_signature: { - status: true, - subject_name: true, - trusted: true, - }, + }, + thread: { + Ext: { + call_stack: true, + start_address: true, + start_address_details: { + address_offset: true, + allocation_base: true, + allocation_protection: true, + allocation_size: true, + allocation_type: true, + base_address: true, + bytes_start_address: true, + compressed_bytes: true, + dest_bytes: true, + dest_bytes_disasm: true, + dest_bytes_disasm_hash: true, + pe: { + Ext: { + legal_copyright: true, + product_version: true, + code_signature: { + status: true, + subject_name: true, + trusted: true, }, - company: true, - description: true, - file_version: true, - imphash: true, - original_file_name: true, - product: true, }, - pe_detected: true, - region_protection: true, - region_size: true, - region_state: true, - strings: true, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, }, + pe_detected: true, + region_protection: true, + region_size: true, + region_state: true, + strings: true, }, }, }, }, - token: { - integrity_level_name: true, - }, }, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 85f11b6c73884..b06f8f4583c7c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9459,7 +9459,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "検索可能なスナップショットを作成するには、エンタープライズライセンスが必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotNoRolloverCalloutBody": "ロールオーバーがホットフェーズで無効な時には、検索可能なスナップショットを作成できません。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "スナップショットリポジトリ名が必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel": "検索可能スナップショットを作成", "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButto": "リクエストを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8d87dda6bba2b..2f0d58b56854f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9483,7 +9483,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "要创建可搜索快照,需要企业许可证。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotNoRolloverCalloutBody": "在热阶段禁用滚动更新后,无法创建可搜索快照。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "快照存储库名称必填。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel": "创建可搜索快照", "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButto": "显示请求", diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts index c318c2d1c26a0..466eab6b6b336 100644 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PANEL_TITLE = 'Visualization PieChart'; - describe('Dashboard Edit Panel', () => { + // FLAKY: https://github.com/elastic/kibana/issues/92114 + describe.skip('Dashboard Edit Panel', () => { before(async () => { await esArchiver.load('dashboard/drilldowns'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/accessibility/apps/search_profiler.ts b/x-pack/test/accessibility/apps/search_profiler.ts index 7fba45175c831..6559d58be6298 100644 --- a/x-pack/test/accessibility/apps/search_profiler.ts +++ b/x-pack/test/accessibility/apps/search_profiler.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const flyout = getService('flyout'); - describe('Accessibility Search Profiler Editor', () => { + // FLAKY: https://github.com/elastic/kibana/issues/91939 + describe.skip('Accessibility Search Profiler Editor', () => { before(async () => { await PageObjects.common.navigateToApp('searchProfiler'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f03756a2885bb..41e94d69d2e9b 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -70,5 +70,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./calendars')); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./saved_objects')); + loadTestFile(require.resolve('./system')); }); } diff --git a/x-pack/test/api_integration/apis/ml/system/capabilities.ts b/x-pack/test/api_integration/apis/ml/system/capabilities.ts new file mode 100644 index 0000000000000..d8ab2a30ef7fb --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/capabilities.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + async function runRequest(user: USER): Promise { + const { body } = await supertest + .get(`/api/ml/ml_capabilities`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + return body; + } + + describe('ml_capabilities', () => { + describe('get capabilities', function () { + it('should be enabled in space', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER); + expect(mlFeatureEnabledInSpace).to.eql(true); + }); + + it('should have upgradeInProgress false', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER); + expect(upgradeInProgress).to.eql(false); + }); + + it('should have full license', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + + it('should have the right number of capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER); + expect(Object.keys(capabilities).length).to.eql(29); + }); + + it('should get viewer capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER); + + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get power user capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER); + + expect(capabilities).to.eql({ + canCreateJob: true, + canDeleteJob: true, + canOpenJob: true, + canCloseJob: true, + canUpdateJob: true, + canForecastJob: true, + canCreateDatafeed: true, + canDeleteDatafeed: true, + canStartStopDatafeed: true, + canUpdateDatafeed: true, + canPreviewDatafeed: true, + canGetFilters: true, + canCreateCalendar: true, + canDeleteCalendar: true, + canCreateFilter: true, + canDeleteFilter: true, + canCreateDataFrameAnalytics: true, + canDeleteDataFrameAnalytics: true, + canStartStopDataFrameAnalytics: true, + canCreateMlAlerts: true, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/system/index.ts b/x-pack/test/api_integration/apis/ml/system/index.ts new file mode 100644 index 0000000000000..68ffd5fa267e9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('system', function () { + loadTestFile(require.resolve('./capabilities')); + loadTestFile(require.resolve('./space_capabilities')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts new file mode 100644 index 0000000000000..cd922bf4bae92 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; + +const idSpaceWithMl = 'space_with_ml'; +const idSpaceNoMl = 'space_no_ml'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const spacesService = getService('spaces'); + const ml = getService('ml'); + + async function runRequest(user: USER, space?: string): Promise { + const { body } = await supertest + .get(`${space ? `/s/${space}` : ''}/api/ml/ml_capabilities`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + return body; + } + + describe('ml_capabilities in spaces', () => { + before(async () => { + await spacesService.create({ id: idSpaceWithMl, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpaceNoMl, name: 'space_two', disabledFeatures: ['ml'] }); + }); + + after(async () => { + await spacesService.delete(idSpaceWithMl); + await spacesService.delete(idSpaceNoMl); + }); + + describe('get capabilities', function () { + it('should be enabled in space - space with ML', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(mlFeatureEnabledInSpace).to.eql(true); + }); + it('should not be enabled in space - space without ML', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(mlFeatureEnabledInSpace).to.eql(false); + }); + + it('should have upgradeInProgress false - space with ML', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(upgradeInProgress).to.eql(false); + }); + it('should have upgradeInProgress false - space without ML', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(upgradeInProgress).to.eql(false); + }); + + it('should have full license - space with ML', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + it('should have full license - space without ML', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + + it('should have the right number of capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(Object.keys(capabilities).length).to.eql(29); + }); + it('should have the right number of capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(Object.keys(capabilities).length).to.eql(29); + }); + + it('should get viewer capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER, idSpaceWithMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get viewer capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER, idSpaceNoMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: false, + canGetJobs: false, + canGetDatafeeds: false, + canGetCalendars: false, + canFindFileStructure: false, + canGetDataFrameAnalytics: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, + }); + }); + + it('should get power user capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(capabilities).to.eql({ + canCreateJob: true, + canDeleteJob: true, + canOpenJob: true, + canCloseJob: true, + canUpdateJob: true, + canForecastJob: true, + canCreateDatafeed: true, + canDeleteDatafeed: true, + canStartStopDatafeed: true, + canUpdateDatafeed: true, + canPreviewDatafeed: true, + canGetFilters: true, + canCreateCalendar: true, + canDeleteCalendar: true, + canCreateFilter: true, + canDeleteFilter: true, + canCreateDataFrameAnalytics: true, + canDeleteDataFrameAnalytics: true, + canStartStopDataFrameAnalytics: true, + canCreateMlAlerts: true, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get power user capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: false, + canGetJobs: false, + canGetDatafeeds: false, + canGetCalendars: false, + canFindFileStructure: false, + canGetDataFrameAnalytics: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, + }); + }); + }); + }); +}; diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.ts b/x-pack/test/apm_api_integration/tests/services/top_services.ts index 3896bc1c6fabc..37f7b09e8b7d2 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.ts @@ -255,6 +255,49 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); + registry.when( + 'APM Services Overview with a basic license when data is loaded excluding transaction events', + { config: 'basic', archives: [archiveName] }, + () => { + it('includes services that only report metric data', async () => { + interface Response { + status: number; + body: APIReturnType<'GET /api/apm/services'>; + } + + const [unfilteredResponse, filteredResponse] = await Promise.all([ + supertest.get(`/api/apm/services?start=${start}&end=${end}`) as Promise, + supertest.get( + `/api/apm/services?start=${start}&end=${end}&kuery=${encodeURIComponent( + 'not (processor.event:transaction)' + )}` + ) as Promise, + ]); + + expect(unfilteredResponse.body.items.length).to.be.greaterThan(0); + + const unfilteredServiceNames = unfilteredResponse.body.items + .map((item) => item.serviceName) + .sort(); + + const filteredServiceNames = filteredResponse.body.items + .map((item) => item.serviceName) + .sort(); + + expect(unfilteredServiceNames).to.eql(filteredServiceNames); + + expect( + filteredResponse.body.items.every((item) => { + // make sure it did not query transaction data + return isEmpty(item.avgResponseTime); + }) + ).to.be(true); + + expect(filteredResponse.body.items.every((item) => !!item.agentName)).to.be(true); + }); + } + ); + registry.when( 'APM Services overview with a trial license when data is loaded', { config: 'trial', archives: [archiveName] }, diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts index f908a369b46d7..c58ca0242a5b5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -101,15 +101,15 @@ export default ({ getService }: FtrProviderContext): void => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest .delete( - `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.subCase!.comments![0].id}?subCaseID=${ - caseInfo.subCase!.id + `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ + caseInfo.subCases![0].id }` ) .set('kbn-xsrf', 'true') .send() .expect(204); const { body } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}` + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` ); expect(body.length).to.eql(0); }); @@ -117,24 +117,24 @@ export default ({ getService }: FtrProviderContext): void => { it('deletes all comments from a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); let { body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}` + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` ); expect(allComments.length).to.eql(2); await supertest - .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send() .expect(204); ({ body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}` + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` )); // no comments for the sub case diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts index 585333291111e..2d8e4c44e023e 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -126,13 +126,13 @@ export default ({ getService }: FtrProviderContext): void => { it('finds comments for a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseID=${caseInfo.subCase!.id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) .send() .expect(200); expect(subCaseComments.total).to.be(2); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts index 1af16f9e54563..264103a2052e5 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_all_comments.ts @@ -83,13 +83,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should get comments from a sub cases', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.subCase!.id}/comments`) + .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .expect(200); expect(comments.length).to.eql(2); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts index 389ec3f088f95..bf63c55938dfe 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should get a sub case comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); const { body: comment }: { body: CommentResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.subCase!.comments![0].id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}`) .expect(200); expect(comment.type).to.be(CommentType.generatedAlert); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts index 86b1c3031cbef..6d9962e938249 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -10,10 +10,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/case/common/constants'; -import { - CollectionWithSubCaseResponse, - CommentType, -} from '../../../../../../plugins/case/common/api'; +import { CaseResponse, CommentType } from '../../../../../../plugins/case/common/api'; import { defaultUser, postCaseReq, @@ -56,42 +53,38 @@ export default ({ getService }: FtrProviderContext): void => { it('patches a comment for a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { - body: patchedSubCase, - }: { body: CollectionWithSubCaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + const { body: patchedSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; const { body: patchedSubCaseUpdatedComment } = await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send({ - id: patchedSubCase.subCase!.comments![1].id, - version: patchedSubCase.subCase!.comments![1].version, + id: patchedSubCase.comments![1].id, + version: patchedSubCase.comments![1].version, comment: newComment, type: CommentType.user, }) .expect(200); - expect(patchedSubCaseUpdatedComment.subCase.comments.length).to.be(2); - expect(patchedSubCaseUpdatedComment.subCase.comments[0].type).to.be( - CommentType.generatedAlert - ); - expect(patchedSubCaseUpdatedComment.subCase.comments[1].type).to.be(CommentType.user); - expect(patchedSubCaseUpdatedComment.subCase.comments[1].comment).to.be(newComment); + expect(patchedSubCaseUpdatedComment.comments.length).to.be(2); + expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert); + expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user); + expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment); }); it('fails to update the generated alert comment type', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send({ - id: caseInfo.subCase!.comments![0].id, - version: caseInfo.subCase!.comments![0].version, + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, type: CommentType.alert, alertId: 'test-id', index: 'test-index', @@ -106,11 +99,11 @@ export default ({ getService }: FtrProviderContext): void => { it('fails to update the generated alert comment by using another generated alert comment', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send({ - id: caseInfo.subCase!.comments![0].id, - version: caseInfo.subCase!.comments![0].version, + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, type: CommentType.generatedAlert, alerts: [{ _id: 'id1' }], index: 'test-index', diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts index fb095c117cdfb..9447f7ad3613c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -393,13 +393,13 @@ export default ({ getService }: FtrProviderContext): void => { // create another sub case just to make sure we get the right comments await createSubCase({ supertest, actionID }); await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseID=${caseInfo.subCase!.id}`) + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) .set('kbn-xsrf', 'true') .send(postCommentUserReq) .expect(200); const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseID=${caseInfo.subCase!.id}`) + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) .send() .expect(200); expect(subCaseComments.total).to.be(2); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts index 8edc3b0d08113..5e761e4d7e33a 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -20,7 +20,7 @@ import { deleteComments, } from '../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../plugins/case/common/api/helpers'; -import { CollectionWithSubCaseResponse } from '../../../../../plugins/case/common/api'; +import { CaseResponse } from '../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -104,7 +104,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should delete the sub cases when deleting a collection', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); const { body } = await supertest .delete(`${CASES_URL}?ids=["${caseInfo.id}"]`) @@ -114,27 +114,25 @@ export default ({ getService }: FtrProviderContext): void => { expect(body).to.eql({}); await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .send() .expect(404); }); it(`should delete a sub case's comments when that case gets deleted`, async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); // there should be two comments on the sub case now - const { - body: patchedCaseWithSubCase, - }: { body: CollectionWithSubCaseResponse } = await supertest + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest .post(`${CASES_URL}/${caseInfo.id}/comments`) .set('kbn-xsrf', 'true') - .query({ subCaseID: caseInfo.subCase!.id }) + .query({ subCaseId: caseInfo.subCases![0].id }) .send(postCommentUserReq) .expect(200); const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.subCase!.comments![1].id + patchedCaseWithSubCase.comments![1].id }`; // make sure we can get the second comment await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts index a2bc0acbcf17c..7514044d376ca 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -265,8 +265,8 @@ export default ({ getService }: FtrProviderContext): void => { supertest, cases: [ { - id: collection.newSubCaseInfo.subCase!.id, - version: collection.newSubCaseInfo.subCase!.version, + id: collection.newSubCaseInfo.subCases![0].id, + version: collection.newSubCaseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -356,7 +356,7 @@ export default ({ getService }: FtrProviderContext): void => { it('correctly counts stats including a collection without sub cases', async () => { // delete the sub case on the collection so that it doesn't have any sub cases await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCase!.id}"]`) + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]`) .set('kbn-xsrf', 'true') .send() .expect(204); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts index 537afbe825068..1d8216ded8b7c 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/delete_sub_cases.ts @@ -19,7 +19,7 @@ import { deleteCaseAction, } from '../../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../../plugins/case/common/api/helpers'; -import { CollectionWithSubCaseResponse } from '../../../../../../plugins/case/common/api'; +import { CaseResponse } from '../../../../../../plugins/case/common/api'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { @@ -40,10 +40,10 @@ export default function ({ getService }: FtrProviderContext) { it('should delete a sub case', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); const { body: subCase } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .send() .expect(200); @@ -57,33 +57,31 @@ export default function ({ getService }: FtrProviderContext) { expect(body).to.eql({}); await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .send() .expect(404); }); it(`should delete a sub case's comments when that case gets deleted`, async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCase?.id).to.not.eql(undefined); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); // there should be two comments on the sub case now - const { - body: patchedCaseWithSubCase, - }: { body: CollectionWithSubCaseResponse } = await supertest + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest .post(`${CASES_URL}/${caseInfo.id}/comments`) .set('kbn-xsrf', 'true') - .query({ subCaseID: caseInfo.subCase!.id }) + .query({ subCaseId: caseInfo.subCases![0].id }) .send(postCommentUserReq) .expect(200); const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.subCase!.comments![1].id + patchedCaseWithSubCase.comments![1].id }`; // make sure we can get the second comment await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCase!.id}"]`) + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) .set('kbn-xsrf', 'true') .send() .expect(204); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts index 3463b37250980..4fd4cd6ec7542 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/find_sub_cases.ts @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext): void => { ...findSubCasesResp, total: 1, // find should not return the comments themselves only the stats - subCases: [{ ...caseInfo.subCase!, comments: [], totalComment: 1, totalAlerts: 2 }], + subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], count_open_cases: 1, }); }); @@ -101,7 +101,7 @@ export default ({ getService }: FtrProviderContext): void => { status: CaseStatuses.closed, }, { - ...subCase2Resp.newSubCaseInfo.subCase, + ...subCase2Resp.newSubCaseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2, @@ -157,8 +157,8 @@ export default ({ getService }: FtrProviderContext): void => { supertest, cases: [ { - id: secondSub.subCase!.id, - version: secondSub.subCase!.version, + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -231,8 +231,8 @@ export default ({ getService }: FtrProviderContext): void => { supertest, cases: [ { - id: secondSub.subCase!.id, - version: secondSub.subCase!.version, + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, status: CaseStatuses['in-progress'], }, ], diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts index cd5a1ed85742f..dff462d78ba82 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/get_sub_case.ts @@ -28,7 +28,7 @@ import { } from '../../../../../../plugins/case/common/api/helpers'; import { AssociationType, - CollectionWithSubCaseResponse, + CaseResponse, SubCaseResponse, } from '../../../../../../plugins/case/common/api'; @@ -53,14 +53,14 @@ export default ({ getService }: FtrProviderContext): void => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .set('kbn-xsrf', 'true') .send() .expect(200); expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( commentsResp({ - comments: [{ comment: defaultCreateSubComment, id: caseInfo.subCase!.comments![0].id }], + comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], associationType: AssociationType.subCase, }) ); @@ -73,15 +73,15 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct number of alerts with multiple types of alerts', async () => { const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: singleAlert }: { body: CollectionWithSubCaseResponse } = await supertest + const { body: singleAlert }: { body: CaseResponse } = await supertest .post(getCaseCommentsUrl(caseInfo.id)) - .query({ subCaseID: caseInfo.subCase!.id }) + .query({ subCaseId: caseInfo.subCases![0].id }) .set('kbn-xsrf', 'true') .send(postCommentAlertReq) .expect(200); const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .set('kbn-xsrf', 'true') .send() .expect(200); @@ -89,10 +89,10 @@ export default ({ getService }: FtrProviderContext): void => { expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( commentsResp({ comments: [ - { comment: defaultCreateSubComment, id: caseInfo.subCase!.comments![0].id }, + { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, { comment: postCommentAlertReq, - id: singleAlert.subCase!.comments![1].id, + id: singleAlert.comments![1].id, }, ], associationType: AssociationType.subCase, diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index 49b3c0b1f465b..5a1da194a721f 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -59,15 +59,15 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], type: 'sub_case', }); const { body: subCase }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCase!.id)) + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) .expect(200); expect(subCase.status).to.eql(CaseStatuses['in-progress']); @@ -102,8 +102,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -159,8 +159,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -239,8 +239,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: collectionWithSecondSub.subCase!.id, - version: collectionWithSecondSub.subCase!.version, + id: collectionWithSecondSub.subCases![0].id, + version: collectionWithSecondSub.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -349,8 +349,8 @@ export default function ({ getService }: FtrProviderContext) { supertest, cases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, status: CaseStatuses['in-progress'], }, ], @@ -450,8 +450,8 @@ export default function ({ getService }: FtrProviderContext) { .send({ subCases: [ { - id: caseInfo.subCase!.id, - version: caseInfo.subCase!.version, + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, type: 'blah', }, ], diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index f6fd2b1a6b3be..c3c37bd20f140 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -26,7 +26,6 @@ import { CaseClientPostRequest, SubCaseResponse, AssociationType, - CollectionWithSubCaseResponse, SubCasesFindResponse, CommentRequest, } from '../../../../plugins/case/common/api'; @@ -159,18 +158,17 @@ export const subCaseResp = ({ interface FormattedCollectionResponse { caseInfo: Partial; - subCase?: Partial; + subCases?: Array>; comments?: Array>; } -export const formatCollectionResponse = ( - caseInfo: CollectionWithSubCaseResponse -): FormattedCollectionResponse => { +export const formatCollectionResponse = (caseInfo: CaseResponse): FormattedCollectionResponse => { + const subCase = removeServerGeneratedPropertiesFromSubCase(caseInfo.subCases?.[0]); return { caseInfo: removeServerGeneratedPropertiesFromCaseCollection(caseInfo), - subCase: removeServerGeneratedPropertiesFromSubCase(caseInfo.subCase), + subCases: subCase ? [subCase] : undefined, comments: removeServerGeneratedPropertiesFromComments( - caseInfo.subCase?.comments ?? caseInfo.comments + caseInfo.subCases?.[0].comments ?? caseInfo.comments ), }; }; @@ -187,10 +185,10 @@ export const removeServerGeneratedPropertiesFromSubCase = ( }; export const removeServerGeneratedPropertiesFromCaseCollection = ( - config: Partial -): Partial => { + config: Partial +): Partial => { // eslint-disable-next-line @typescript-eslint/naming-convention - const { closed_at, created_at, updated_at, version, subCase, ...rest } = config; + const { closed_at, created_at, updated_at, version, subCases, ...rest } = config; return rest; }; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 7aee6170c3d5a..3ade7ef96f9dd 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -17,7 +17,7 @@ import { CaseConnector, ConnectorTypes, CasePostRequest, - CollectionWithSubCaseResponse, + CaseResponse, SubCasesFindResponse, CaseStatuses, SubCasesResponse, @@ -120,7 +120,7 @@ export const defaultCreateSubPost = postCollectionReq; * Response structure for the createSubCase and createSubCaseComment functions. */ export interface CreateSubCaseResp { - newSubCaseInfo: CollectionWithSubCaseResponse; + newSubCaseInfo: CaseResponse; modifiedSubCases?: SubCasesResponse; } diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 7711338b44697..683d57081a267 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KbnClient } from '@kbn/dev-utils'; +import { KbnClient } from '@kbn/test'; import { ApiResponse, Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap new file mode 100644 index 0000000000000..43771b00525cc --- /dev/null +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`discover Discover Generate CSV: archived search generates a report with data 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"order_date\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\",\\"\\"Women's Accessories\\"\\",\\"\\"Men's Accessories\\"\\"]\\",EUR,19,716724,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0687606876\\"\\",\\"\\"ZO0290502905\\"\\",\\"\\"ZO0126701267\\"\\",\\"\\"ZO0308503085\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Shoes\\"\\",\\"\\"Women's Clothing\\"\\"]\\",EUR,45,591503,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0006400064\\"\\",\\"\\"ZO0150601506\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0638206382\\"\\",\\"\\"ZO0038800388\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0297602976\\"\\",\\"\\"ZO0565605656\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0561405614\\"\\",\\"\\"ZO0281602816\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\"]\\",EUR,41,591636,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0385003850\\"\\",\\"\\"ZO0408604086\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0505605056\\"\\",\\"\\"ZO0513605136\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0276702767\\"\\",\\"\\"ZO0291702917\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0046600466\\"\\",\\"\\"ZO0050800508\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Clothing\\"\\",\\"\\"Men's Shoes\\"\\"]\\",EUR,48,590970,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0455604556\\"\\",\\"\\"ZO0680806808\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Clothing\\"\\",\\"\\"Women's Shoes\\"\\"]\\",EUR,46,591299,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0229002290\\"\\",\\"\\"ZO0674406744\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0529905299\\"\\",\\"\\"ZO0617006170\\"\\"]\\" +" +`; + +exports[`discover Discover Generate CSV: archived search generates a report with filtered data 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"order_date\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\",\\"\\"Women's Accessories\\"\\",\\"\\"Men's Accessories\\"\\"]\\",EUR,19,716724,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0687606876\\"\\",\\"\\"ZO0290502905\\"\\",\\"\\"ZO0126701267\\"\\",\\"\\"ZO0308503085\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Shoes\\"\\",\\"\\"Women's Clothing\\"\\"]\\",EUR,45,591503,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0006400064\\"\\",\\"\\"ZO0150601506\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0638206382\\"\\",\\"\\"ZO0038800388\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0297602976\\"\\",\\"\\"ZO0565605656\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0561405614\\"\\",\\"\\"ZO0281602816\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\"]\\",EUR,41,591636,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0385003850\\"\\",\\"\\"ZO0408604086\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0505605056\\"\\",\\"\\"ZO0513605136\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0276702767\\"\\",\\"\\"ZO0291702917\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0046600466\\"\\",\\"\\"ZO0050800508\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Clothing\\"\\",\\"\\"Men's Shoes\\"\\"]\\",EUR,48,590970,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0455604556\\"\\",\\"\\"ZO0680806808\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Clothing\\"\\",\\"\\"Women's Shoes\\"\\"]\\",EUR,46,591299,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0229002290\\"\\",\\"\\"ZO0674406744\\"\\"]\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0529905299\\"\\",\\"\\"ZO0617006170\\"\\"]\\" +" +`; + +exports[`discover Discover Generate CSV: new search generates a report with data 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user +" +`; diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index a0f10774e7fa6..dfc44a8e0e12d 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const PageObjects = getPageObjects(['reporting', 'common', 'discover']); + const PageObjects = getPageObjects(['reporting', 'common', 'discover', 'timePicker']); const filterBar = getService('filterBar'); describe('Discover', () => { @@ -31,7 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('Generate CSV button', () => { + describe('Generate CSV: new search', () => { beforeEach(() => PageObjects.common.navigateToApp('discover')); it('is not available if new', async () => { @@ -69,14 +69,83 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.reporting.setTimepickerInDataRange(); await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); }); it('generates a report with no data', async () => { await PageObjects.reporting.setTimepickerInNoDataRange(); await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatchInline(` + " + " + `); + }); + }); + + describe('Generate CSV: archived search', () => { + before(async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + }); + + after(async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + beforeEach(() => PageObjects.common.navigateToApp('discover')); + + it('generates a report with data', async () => { + await PageObjects.discover.loadSavedSearch('Ecommerce Data'); + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await PageObjects.reporting.openCsvReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); + }); + + it('generates a report with filtered data', async () => { + await PageObjects.discover.loadSavedSearch('Ecommerce Data'); + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + // filter and re-save + await filterBar.addFilter('currency', 'is', 'EUR'); + await PageObjects.discover.saveSearch(`Ecommerce Data: EUR Filtered`); + + await PageObjects.reporting.openCsvReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); }); }); }); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 6c9eb24070d8f..b9c1767e4a8cf 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -15,7 +15,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - describe('overview page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/89072 + describe.skip('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index 7d33680f1dc31..746df14d31ac4 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -140,8 +140,8 @@ export function ReportingPageProvider({ getService, getPageObjects }: FtrProvide async setTimepickerInDataRange() { log.debug('Reporting:setTimepickerInDataRange'); - const fromTime = 'Sep 19, 2015 @ 06:31:44.000'; - const toTime = 'Sep 19, 2015 @ 18:01:44.000'; + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } diff --git a/yarn.lock b/yarn.lock index 92c67cda974c3..6c706cffebaaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14954,6 +14954,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"