diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index a5b27c67eaca15..0131bca3e6c507 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import actionsObj from './actions.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 116 | 0 | 116 | 7 | + ## Server ### Setup diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 5b0e69c14f58b0..241b6b82598c3f 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import advancedSettingsObj from './advanced_settings.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 23 | 0 | 22 | 1 | + ## Client ### Setup diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index a0294a02d9f109..5dce4a9a2c7b17 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import alertingObj from './alerting.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 213 | 0 | 213 | 15 | + ## Client ### Setup diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index d037dc99e1a542..e2a4ee9e7ea7ab 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import apmObj from './apm.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 37 | 0 | 37 | 30 | + ## Client ### Setup diff --git a/api_docs/apm_oss.mdx b/api_docs/apm_oss.mdx index 73707040013788..2a03249734f9c9 100644 --- a/api_docs/apm_oss.mdx +++ b/api_docs/apm_oss.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import apmOssObj from './apm_oss.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 4 | 0 | + ## Server ### Setup diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index e6b648e38cdc34..d9727cb817e26a 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import bannersObj from './banners.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 9 | 0 | 9 | 0 | + ## Common ### Interfaces diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index faf3d7eefc7f9f..217e190831313b 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import bfetchObj from './bfetch.json'; +Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 73 | 1 | 62 | 2 | + ## Client ### Start diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 75ac78d571bc99..9d8707ab778e6b 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import canvasObj from './canvas.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 6 | 0 | 5 | 3 | + ## Client ### Setup diff --git a/api_docs/cases.json b/api_docs/cases.json index 55ec344cb0bcb2..bc92995dff6e91 100644 --- a/api_docs/cases.json +++ b/api_docs/cases.json @@ -1035,7 +1035,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 53 + "lineNumber": 49 }, "deprecated": false, "children": [ @@ -1051,7 +1051,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 53 + "lineNumber": 49 }, "deprecated": false, "isRequired": true @@ -1074,7 +1074,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 59 + "lineNumber": 55 }, "deprecated": false, "children": [ @@ -1091,7 +1091,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 60 + "lineNumber": 56 }, "deprecated": false, "isRequired": true @@ -1108,7 +1108,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 61 + "lineNumber": 57 }, "deprecated": false, "isRequired": true @@ -1129,7 +1129,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 75 + "lineNumber": 71 }, "deprecated": false, "children": [ @@ -1145,7 +1145,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 76 + "lineNumber": 72 }, "deprecated": false, "isRequired": true @@ -1170,7 +1170,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 24 + "lineNumber": 20 }, "deprecated": true, "references": [], @@ -1187,7 +1187,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 24 + "lineNumber": 20 }, "deprecated": false, "isRequired": true @@ -1634,62 +1634,6 @@ "returnComment": [], "initialIsOpen": false }, - { - "parentPluginId": "cases", - "id": "def-common.OmitProp", - "type": "Function", - "tags": [], - "label": "OmitProp", - "description": [], - "signature": [ - "(o: O, k: K) => Pick>" - ], - "source": { - "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 17 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "cases", - "id": "def-common.OmitProp.$1", - "type": "Uncategorized", - "tags": [], - "label": "o", - "description": [], - "signature": [ - "O" - ], - "source": { - "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 17 - }, - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "cases", - "id": "def-common.OmitProp.$2", - "type": "Uncategorized", - "tags": [], - "label": "k", - "description": [], - "signature": [ - "K" - ], - "source": { - "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 17 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "cases", "id": "def-common.throwErrors", @@ -1704,7 +1648,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 55 + "lineNumber": 51 }, "deprecated": false, "children": [ @@ -1720,7 +1664,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 55 + "lineNumber": 51 }, "deprecated": false, "isRequired": true @@ -5278,7 +5222,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; })[]" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; })[]" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5335,7 +5279,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5392,7 +5336,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5506,7 +5450,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5563,7 +5507,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -7187,7 +7131,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }, \"updated_at\" | \"owner\" | \"created_at\" | \"created_by\" | \"updated_by\" | \"closure_type\"> & { connector: ", + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }, \"updated_at\" | \"owner\" | \"created_at\" | \"created_by\" | \"updated_by\" | \"closure_type\"> & { connector: ", { "pluginId": "cases", "scope": "common", @@ -7359,6 +7303,40 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "cases", + "id": "def-common.MAX_CONCURRENT_SEARCHES", + "type": "number", + "tags": [], + "label": "MAX_CONCURRENT_SEARCHES", + "description": [], + "signature": [ + "10" + ], + "source": { + "path": "x-pack/plugins/cases/common/constants.ts", + "lineNumber": 97 + }, + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-common.MAX_DOCS_PER_PAGE", + "type": "number", + "tags": [], + "label": "MAX_DOCS_PER_PAGE", + "description": [], + "signature": [ + "10000" + ], + "source": { + "path": "x-pack/plugins/cases/common/constants.ts", + "lineNumber": 96 + }, + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "cases", "id": "def-common.MAX_GENERATED_ALERTS_PER_SUB_CASE", @@ -9365,6 +9343,8 @@ "<[", "IntersectionC", "<[", + "IntersectionC", + "<[", "TypeC", "<{ connector: ", "IntersectionC", @@ -9572,9 +9552,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>, ", + "; }>]>, ", "TypeC", "<{ created_at: ", "StringC", @@ -9701,6 +9683,8 @@ "label": "CaseConfigureAttributesRt", "description": [], "signature": [ + "IntersectionC", + "<[", "IntersectionC", "<[", "TypeC", @@ -9910,9 +9894,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>, ", + "; }>]>, ", "TypeC", "<{ created_at: ", "StringC", @@ -10019,6 +10005,8 @@ "<[", "IntersectionC", "<[", + "IntersectionC", + "<[", "TypeC", "<{ connector: ", "IntersectionC", @@ -10226,9 +10214,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>, ", + "; }>]>, ", "TypeC", "<{ created_at: ", "StringC", @@ -12728,7 +12718,7 @@ "IntersectionC", "<[", "PartialC", - ", ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", - "StringC", - "; }, \"connector\" | \"closure_type\">>, ", + "<\"close-by-pushing\">]>; }>, ", "TypeC", "<{ version: ", "StringC", @@ -12957,6 +12945,8 @@ "label": "CasesConfigureRequestRt", "description": [], "signature": [ + "IntersectionC", + "<[", "TypeC", "<{ connector: ", "IntersectionC", @@ -13164,9 +13154,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>" + "; }>]>" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 20d371cbcdc6e0..0f9cbe5364b630 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import casesObj from './cases.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 406 | 0 | 381 | 13 | + ## Client ### Start diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 1cbdb2f19a6fbb..95d0bd527e631b 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import chartsObj from './charts.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 190 | 2 | 159 | 1 | + ## Client ### Start diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index 4a61ce885f1548..f9d44f9c8d08ba 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import cloudObj from './cloud.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 20 | 0 | 20 | 0 | + ## Client ### Setup diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 3514fa2697aaa0..74a7fadf7a5c37 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import consoleObj from './console.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 2 | 0 | + ## Server ### Setup diff --git a/api_docs/core.mdx b/api_docs/core.mdx index 77050dd72894a8..31889ec1042b84 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import coreObj from './core.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2273 | 148 | 1052 | 26 | + ## Client ### Classes diff --git a/api_docs/core_application.mdx b/api_docs/core_application.mdx index 76d2d95f21e05f..ce165343346322 100644 --- a/api_docs/core_application.mdx +++ b/api_docs/core_application.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import coreApplicationObj from './core_application.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2273 | 148 | 1052 | 26 | + ## Client ### Classes diff --git a/api_docs/core_chrome.mdx b/api_docs/core_chrome.mdx index a9e100b79a91ec..3fbc87ad30fffc 100644 --- a/api_docs/core_chrome.mdx +++ b/api_docs/core_chrome.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import coreChromeObj from './core_chrome.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2273 | 148 | 1052 | 26 | + ## Client ### Interfaces diff --git a/api_docs/core_http.mdx b/api_docs/core_http.mdx index ea545eb6d70def..4f15e1a1ce90c5 100644 --- a/api_docs/core_http.mdx +++ b/api_docs/core_http.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import coreHttpObj from './core_http.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2273 | 148 | 1052 | 26 | + ## Client ### Interfaces diff --git a/api_docs/core_saved_objects.mdx b/api_docs/core_saved_objects.mdx index d534853f7f29db..fd3f96e081c9e3 100644 --- a/api_docs/core_saved_objects.mdx +++ b/api_docs/core_saved_objects.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import coreSavedObjectsObj from './core_saved_objects.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2273 | 148 | 1052 | 26 | + ## Client ### Classes diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index bddd6f91796fc7..de34ce88a77be5 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dashboardObj from './dashboard.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 145 | 1 | 133 | 9 | + ## Client ### Setup diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index 1ef8820befc41c..4af7f6f96cf23c 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dashboardEnhancedObj from './dashboard_enhanced.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 51 | 0 | 50 | 0 | + ## Client ### Setup diff --git a/api_docs/dashboard_mode.mdx b/api_docs/dashboard_mode.mdx index 2108754adbd5db..28b68e29bd5f26 100644 --- a/api_docs/dashboard_mode.mdx +++ b/api_docs/dashboard_mode.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dashboardModeObj from './dashboard_mode.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 11 | 0 | 11 | 0 | + ## Server ### Classes diff --git a/api_docs/data.json b/api_docs/data.json index 2f9e74584603fc..993dcec522d5e8 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -12344,34 +12344,6 @@ "lineNumber": 222 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", - "lineNumber": 11 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", - "lineNumber": 16 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx", - "lineNumber": 25 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx", - "lineNumber": 162 - } - }, { "plugin": "infra", "link": { @@ -12848,6 +12820,20 @@ "lineNumber": 11 } }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", + "lineNumber": 11 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", + "lineNumber": 16 + } + }, { "plugin": "ml", "link": { @@ -15759,20 +15745,6 @@ "lineNumber": 11 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 25 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 28 - } - }, { "plugin": "infra", "link": { @@ -16382,48 +16354,6 @@ "lineNumber": 57 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 14 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 27 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 213 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 234 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", - "lineNumber": 12 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", - "lineNumber": 17 - } - }, { "plugin": "observability", "link": { @@ -34346,34 +34276,6 @@ "lineNumber": 222 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", - "lineNumber": 11 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", - "lineNumber": 16 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx", - "lineNumber": 25 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx", - "lineNumber": 162 - } - }, { "plugin": "infra", "link": { @@ -34850,6 +34752,20 @@ "lineNumber": 11 } }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", + "lineNumber": 11 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", + "lineNumber": 16 + } + }, { "plugin": "ml", "link": { diff --git a/api_docs/data.mdx b/api_docs/data.mdx index f585b23a16a0c6..bd48fb09df8b30 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataObj from './data.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Client ### Setup diff --git a/api_docs/data_autocomplete.mdx b/api_docs/data_autocomplete.mdx index c2231648ac905d..7d18feb2140dd4 100644 --- a/api_docs/data_autocomplete.mdx +++ b/api_docs/data_autocomplete.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataAutocompleteObj from './data_autocomplete.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Client ### Interfaces diff --git a/api_docs/data_enhanced.mdx b/api_docs/data_enhanced.mdx index 07a00b908e7b26..1e6709794d3c49 100644 --- a/api_docs/data_enhanced.mdx +++ b/api_docs/data_enhanced.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataEnhancedObj from './data_enhanced.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 16 | 0 | 16 | 2 | + ## Client ### Start diff --git a/api_docs/data_field_formats.mdx b/api_docs/data_field_formats.mdx index faeba4ac56454d..c13562eba142df 100644 --- a/api_docs/data_field_formats.mdx +++ b/api_docs/data_field_formats.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataFieldFormatsObj from './data_field_formats.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Client ### Consts, variables and types diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index af5ddec4809768..056442ee29d668 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -6117,34 +6117,6 @@ "lineNumber": 222 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", - "lineNumber": 11 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", - "lineNumber": 16 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx", - "lineNumber": 25 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx", - "lineNumber": 162 - } - }, { "plugin": "infra", "link": { @@ -6621,6 +6593,20 @@ "lineNumber": 11 } }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", + "lineNumber": 11 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/util/field_types_utils.ts", + "lineNumber": 16 + } + }, { "plugin": "ml", "link": { @@ -9532,20 +9518,6 @@ "lineNumber": 11 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 25 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 28 - } - }, { "plugin": "infra", "link": { @@ -10155,48 +10127,6 @@ "lineNumber": 57 } }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 14 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 27 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 213 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 234 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", - "lineNumber": 12 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", - "lineNumber": 17 - } - }, { "plugin": "observability", "link": { diff --git a/api_docs/data_index_patterns.mdx b/api_docs/data_index_patterns.mdx index df226467aefdce..8313619594a753 100644 --- a/api_docs/data_index_patterns.mdx +++ b/api_docs/data_index_patterns.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataIndexPatternsObj from './data_index_patterns.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Server ### Functions diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index ae804971a43194..751cd14dc7c5be 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataQueryObj from './data_query.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Client ### Functions diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 370bd2ffd101e8..d07be7dfc62aa2 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataSearchObj from './data_search.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Client ### Functions diff --git a/api_docs/data_ui.mdx b/api_docs/data_ui.mdx index 9eedffdce76f61..c31b561bd4c803 100644 --- a/api_docs/data_ui.mdx +++ b/api_docs/data_ui.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import dataUiObj from './data_ui.json'; +Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. + +Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3809 | 107 | 3257 | 75 | + ## Client ### Functions diff --git a/api_docs/data_visualizer.json b/api_docs/data_visualizer.json index b4544a03817909..c965a0a5923ac4 100644 --- a/api_docs/data_visualizer.json +++ b/api_docs/data_visualizer.json @@ -3,7 +3,69 @@ "client": { "classes": [], "functions": [], - "interfaces": [], + "interfaces": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-public.IndexDataVisualizerViewProps", + "type": "Interface", + "tags": [], + "label": "IndexDataVisualizerViewProps", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx", + "lineNumber": 119 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-public.IndexDataVisualizerViewProps.currentIndexPattern", + "type": "Object", + "tags": [], + "label": "currentIndexPattern", + "description": [], + "signature": [ + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataIndexPatternsPluginApi", + "section": "def-common.IndexPattern", + "text": "IndexPattern" + } + ], + "source": { + "path": "x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx", + "lineNumber": 120 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-public.IndexDataVisualizerViewProps.currentSavedSearch", + "type": "CompoundType", + "tags": [], + "label": "currentSavedSearch", + "description": [], + "signature": [ + { + "pluginId": "dataVisualizer", + "scope": "common", + "docId": "kibDataVisualizerPluginApi", + "section": "def-common.SavedSearchSavedObject", + "text": "SavedSearchSavedObject" + }, + " | null" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx", + "lineNumber": 121 + }, + "deprecated": false + } + ], + "initialIsOpen": false + } + ], "enums": [], "misc": [], "objects": [], @@ -15,11 +77,11 @@ "label": "DataVisualizerPluginStart", "description": [], "signature": [ - "{ getFileDataVisualizerComponent: () => Promise>; getMaxBytesFormatted: () => string; }" + "{ getFileDataVisualizerComponent: () => Promise>; getIndexDataVisualizerComponent: () => Promise>; getMaxBytesFormatted: () => string; }" ], "source": { "path": "x-pack/plugins/data_visualizer/public/plugin.ts", - "lineNumber": 33 + "lineNumber": 38 }, "deprecated": false, "lifecycle": "start", @@ -46,8 +108,8 @@ "label": "DataVisualizerTableState", "description": [], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 14 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 19 }, "deprecated": false, "children": [ @@ -59,8 +121,8 @@ "label": "pageSize", "description": [], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 15 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 20 }, "deprecated": false }, @@ -72,8 +134,8 @@ "label": "pageIndex", "description": [], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 16 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 21 }, "deprecated": false }, @@ -85,8 +147,8 @@ "label": "sortField", "description": [], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 17 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 22 }, "deprecated": false }, @@ -98,8 +160,8 @@ "label": "sortDirection", "description": [], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 18 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 23 }, "deprecated": false }, @@ -114,8 +176,8 @@ "string[]" ], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 19 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 24 }, "deprecated": false }, @@ -130,8 +192,8 @@ "string[]" ], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 20 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 25 }, "deprecated": false }, @@ -143,8 +205,579 @@ "label": "showDistributions", "description": [], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 21 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 26 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.DocumentCountBuckets", + "type": "Interface", + "tags": [], + "label": "DocumentCountBuckets", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 22 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-common.DocumentCountBuckets.Unnamed", + "type": "Any", + "tags": [], + "label": "Unnamed", + "description": [], + "signature": [ + "any" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 23 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.DocumentCounts", + "type": "Interface", + "tags": [], + "label": "DocumentCounts", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 26 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-common.DocumentCounts.buckets", + "type": "Object", + "tags": [], + "label": "buckets", + "description": [], + "signature": [ + { + "pluginId": "dataVisualizer", + "scope": "common", + "docId": "kibDataVisualizerPluginApi", + "section": "def-common.DocumentCountBuckets", + "text": "DocumentCountBuckets" + }, + " | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 27 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.DocumentCounts.interval", + "type": "number", + "tags": [], + "label": "interval", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 28 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldRequestConfig", + "type": "Interface", + "tags": [], + "label": "FieldRequestConfig", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 16 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldRequestConfig.fieldName", + "type": "string", + "tags": [], + "label": "fieldName", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 17 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldRequestConfig.type", + "type": "CompoundType", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "\"number\" | \"boolean\" | \"date\" | \"keyword\" | \"text\" | \"ip\" | \"geo_point\" | \"geo_shape\" | \"unknown\"" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 18 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldRequestConfig.cardinality", + "type": "number", + "tags": [], + "label": "cardinality", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 19 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats", + "type": "Interface", + "tags": [], + "label": "FieldVisStats", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 31 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.cardinality", + "type": "number", + "tags": [], + "label": "cardinality", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 32 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.count", + "type": "number", + "tags": [], + "label": "count", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 33 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.sampleCount", + "type": "number", + "tags": [], + "label": "sampleCount", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 34 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.trueCount", + "type": "number", + "tags": [], + "label": "trueCount", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 35 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.falseCount", + "type": "number", + "tags": [], + "label": "falseCount", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 36 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.earliest", + "type": "number", + "tags": [], + "label": "earliest", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 37 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.latest", + "type": "number", + "tags": [], + "label": "latest", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 38 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.documentCounts", + "type": "Object", + "tags": [], + "label": "documentCounts", + "description": [], + "signature": [ + "{ buckets?: ", + { + "pluginId": "dataVisualizer", + "scope": "common", + "docId": "kibDataVisualizerPluginApi", + "section": "def-common.DocumentCountBuckets", + "text": "DocumentCountBuckets" + }, + " | undefined; interval?: number | undefined; } | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 39 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.avg", + "type": "number", + "tags": [], + "label": "avg", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 43 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.distribution", + "type": "Object", + "tags": [], + "label": "distribution", + "description": [], + "signature": [ + "{ percentiles: ", + { + "pluginId": "dataVisualizer", + "scope": "common", + "docId": "kibDataVisualizerPluginApi", + "section": "def-common.Percentile", + "text": "Percentile" + }, + "[]; maxPercentile: number; minPercentile: 0; } | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 44 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.fieldName", + "type": "string", + "tags": [], + "label": "fieldName", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 49 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.isTopValuesSampled", + "type": "CompoundType", + "tags": [], + "label": "isTopValuesSampled", + "description": [], + "signature": [ + "boolean | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 50 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.max", + "type": "number", + "tags": [], + "label": "max", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 51 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.median", + "type": "number", + "tags": [], + "label": "median", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 52 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.min", + "type": "number", + "tags": [], + "label": "min", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 53 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.topValues", + "type": "Array", + "tags": [], + "label": "topValues", + "description": [], + "signature": [ + "{ key: React.ReactText; doc_count: number; }[] | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 54 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.topValuesSampleSize", + "type": "number", + "tags": [], + "label": "topValuesSampleSize", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 55 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.topValuesSamplerShardSize", + "type": "number", + "tags": [], + "label": "topValuesSamplerShardSize", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 56 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.examples", + "type": "Array", + "tags": [], + "label": "examples", + "description": [], + "signature": [ + "(string | object)[] | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 57 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.timeRangeEarliest", + "type": "number", + "tags": [], + "label": "timeRangeEarliest", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 58 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.FieldVisStats.timeRangeLatest", + "type": "number", + "tags": [], + "label": "timeRangeLatest", + "description": [], + "signature": [ + "number | undefined" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 59 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.Percentile", + "type": "Interface", + "tags": [], + "label": "Percentile", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 10 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "dataVisualizer", + "id": "def-common.Percentile.percent", + "type": "number", + "tags": [], + "label": "percent", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 11 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.Percentile.minValue", + "type": "number", + "tags": [], + "label": "minValue", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 12 + }, + "deprecated": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.Percentile.maxValue", + "type": "number", + "tags": [], + "label": "maxValue", + "description": [], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/field_request_config.ts", + "lineNumber": 13 }, "deprecated": false } @@ -216,8 +849,8 @@ "any[]" ], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 10 + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 17 }, "deprecated": false, "initialIsOpen": false @@ -230,11 +863,11 @@ "label": "JobFieldType", "description": [], "signature": [ - "\"number\" | \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"geo_point\" | \"geo_shape\" | \"unknown\"" + "\"number\" | \"boolean\" | \"date\" | \"keyword\" | \"text\" | \"ip\" | \"geo_point\" | \"geo_shape\" | \"unknown\"" ], "source": { - "path": "x-pack/plugins/data_visualizer/common/types.ts", - "lineNumber": 12 + "path": "x-pack/plugins/data_visualizer/common/types/job_field_type.ts", + "lineNumber": 9 }, "deprecated": false, "initialIsOpen": false @@ -287,6 +920,47 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.OMIT_FIELDS", + "type": "Array", + "tags": [], + "label": "OMIT_FIELDS", + "description": [], + "signature": [ + "string[]" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/constants.ts", + "lineNumber": 33 + }, + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "dataVisualizer", + "id": "def-common.SavedSearchSavedObject", + "type": "Type", + "tags": [], + "label": "SavedSearchSavedObject", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "public", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-public.SimpleSavedObject", + "text": "SimpleSavedObject" + }, + "" + ], + "source": { + "path": "x-pack/plugins/data_visualizer/common/types/index.ts", + "lineNumber": 29 + }, + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "dataVisualizer", "id": "def-common.UI_SETTING_MAX_FILE_SIZE", @@ -325,4 +999,4 @@ } ] } -} +} \ No newline at end of file diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 07d4fc76d448a2..384fd07bd11762 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -1,29 +1,42 @@ --- -id: kibFileDataVisualizerPluginApi -slug: /kibana-dev-docs/fileDataVisualizerPluginApi -title: fileDataVisualizer +id: kibDataVisualizerPluginApi +slug: /kibana-dev-docs/dataVisualizerPluginApi +title: dataVisualizer image: https://source.unsplash.com/400x175/?github -summary: API docs for the fileDataVisualizer plugin +summary: API docs for the dataVisualizer plugin date: 2020-11-16 -tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileDataVisualizer'] +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- -import fileDataVisualizerObj from './file_data_visualizer.json'; +import dataVisualizerObj from './data_visualizer.json'; + + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 59 | 1 | 59 | 0 | ## Client ### Start - + + +### Interfaces + ## Common ### Objects - + ### Interfaces - + ### Consts, variables and types - + diff --git a/api_docs/deprecations.mdx b/api_docs/deprecations.mdx index b5fe8d1e31da0a..d9261b943d1708 100644 --- a/api_docs/deprecations.mdx +++ b/api_docs/deprecations.mdx @@ -1250,8 +1250,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [utils.d.ts#L37](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts#L37) | - | | | [edit_utils.d.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts#L8) | - | | | [edit_utils.d.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts#L11) | - | -| | [actions_panel.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx#L25) | - | -| | [actions_panel.tsx#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx#L28) | - | | | [new_job_utils.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts#L11) | - | | | [new_job_utils.ts#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts#L34) | - | | | [exploration_query_bar.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx#L14) | - | @@ -1260,12 +1258,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [editor.tsx#L62](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx#L62) | - | | | [custom_urls.tsx#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx#L40) | - | | | [custom_urls.tsx#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx#L57) | - | -| | [lens_utils.ts#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L14) | - | -| | [lens_utils.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L27) | - | -| | [lens_utils.ts#L213](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L213) | - | -| | [lens_utils.ts#L234](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L234) | - | -| | [actions.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts#L12) | - | -| | [actions.ts#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts#L17) | - | | | [explorer_query_bar.tsx#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx#L17) | - | | | [explorer_query_bar.tsx#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx#L31) | - | | | [explorer_query_bar.tsx#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx#L100) | - | @@ -1275,8 +1267,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [common.ts#L222](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/components/data_grid/common.ts#L222) | - | | | [field_types_utils.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.ts#L11) | - | | | [field_types_utils.ts#L16](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.ts#L16) | - | -| | [page.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx#L25) | - | -| | [page.tsx#L162](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx#L162) | - | | | [field_types_utils.test.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L8) | - | | | [field_types_utils.test.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L19) | - | | | [field_types_utils.test.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L40) | - | @@ -1286,8 +1276,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [common.ts#L222](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/components/data_grid/common.ts#L222) | - | | | [field_types_utils.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.ts#L11) | - | | | [field_types_utils.ts#L16](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.ts#L16) | - | -| | [page.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx#L25) | - | -| | [page.tsx#L162](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx#L162) | - | | | [field_types_utils.test.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L8) | - | | | [field_types_utils.test.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L19) | - | | | [field_types_utils.test.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L40) | - | @@ -1316,8 +1304,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [utils.d.ts#L37](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts#L37) | - | | | [edit_utils.d.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts#L8) | - | | | [edit_utils.d.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts#L11) | - | -| | [actions_panel.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx#L25) | - | -| | [actions_panel.tsx#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx#L28) | - | | | [new_job_utils.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts#L11) | - | | | [new_job_utils.ts#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts#L34) | - | | | [exploration_query_bar.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx#L14) | - | @@ -1326,12 +1312,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [editor.tsx#L62](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/editor.tsx#L62) | - | | | [custom_urls.tsx#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx#L40) | - | | | [custom_urls.tsx#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx#L57) | - | -| | [lens_utils.ts#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L14) | - | -| | [lens_utils.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L27) | - | -| | [lens_utils.ts#L213](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L213) | - | -| | [lens_utils.ts#L234](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts#L234) | - | -| | [actions.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts#L12) | - | -| | [actions.ts#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts#L17) | - | | | [explorer_query_bar.tsx#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx#L17) | - | | | [explorer_query_bar.tsx#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx#L31) | - | | | [explorer_query_bar.tsx#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx#L100) | - | @@ -1341,8 +1321,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [common.ts#L222](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/components/data_grid/common.ts#L222) | - | | | [field_types_utils.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.ts#L11) | - | | | [field_types_utils.ts#L16](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.ts#L16) | - | -| | [page.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx#L25) | - | -| | [page.tsx#L162](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx#L162) | - | | | [field_types_utils.test.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L8) | - | | | [field_types_utils.test.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L19) | - | | | [field_types_utils.test.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/ml/public/application/util/field_types_utils.test.ts#L40) | - | diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 63abb581b1d3ec..89d67f54866f23 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import devToolsObj from './dev_tools.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 10 | 0 | 8 | 2 | + ## Client ### Setup diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index b5f6cefde7cc42..d4720530b58487 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import discoverObj from './discover.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 64 | 0 | 51 | 6 | + ## Client ### Setup diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index 06037e1190c0d8..fb8842cd56bfed 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import discoverEnhancedObj from './discover_enhanced.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 39 | 0 | 37 | 2 | + ## Client ### Classes diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index eafc8543b7ce13..46a877be6ac243 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import embeddableObj from './embeddable.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 449 | 4 | 379 | 3 | + ## Client ### Setup diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 58800f0e7d3435..8680c6807bbcaa 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import embeddableEnhancedObj from './embeddable_enhanced.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 14 | 0 | 14 | 0 | + ## Client ### Setup diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index 654427d83c3598..e152ec4aec9a85 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import encryptedSavedObjectsObj from './encrypted_saved_objects.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 30 | 0 | 28 | 3 | + ## Server ### Setup diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 1f9dfaae078f40..8156918dcefe49 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import enterpriseSearchObj from './enterprise_search.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 2 | 0 | + ## Server ### Objects diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 2fd04e135e3fd3..e317299f0b48aa 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import esUiSharedObj from './es_ui_shared.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 88 | 4 | 86 | 1 | + ## Client ### Objects diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 6948a63cf85c93..409d6ad6d21c29 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import eventLogObj from './event_log.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 70 | 0 | 70 | 4 | + ## Server ### Setup diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index a57e91bf2a333b..e0544d866766ed 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import expressionsObj from './expressions.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 1871 | 57 | 1444 | 5 | + ## Client ### Setup diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 99776f69301592..377945d852ddd6 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import featuresObj from './features.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 215 | 0 | 97 | 2 | + ## Client ### Setup diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index aa4af1121e1183..acd0ed52e41826 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import fileUploadObj from './file_upload.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 128 | 4 | 128 | 1 | + ## Client ### Start diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index 1388184f7a9480..2113069fe47e07 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import fleetObj from './fleet.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 1071 | 15 | 981 | 8 | + ## Client ### Setup diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 2e88a252eb35dd..041b9aba8ec7c8 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import globalSearchObj from './global_search.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 68 | 0 | 14 | 5 | + ## Client ### Setup diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 0a8bac237b045b..23c58467b6a986 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import homeObj from './home.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 89 | 0 | 65 | 5 | + ## Client ### Setup diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index f6aff00be5c616..a16206ad3d3610 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import indexLifecycleManagementObj from './index_lifecycle_management.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 5 | 0 | 5 | 0 | + ## Client ### Interfaces diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 0f22d279f7ecf9..9190012ac0b293 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import indexManagementObj from './index_management.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 162 | 12 | 157 | 3 | + ## Client ### Functions diff --git a/api_docs/index_pattern_field_editor.mdx b/api_docs/index_pattern_field_editor.mdx index 7c057e8706f7de..1627a18e66724a 100644 --- a/api_docs/index_pattern_field_editor.mdx +++ b/api_docs/index_pattern_field_editor.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import indexPatternFieldEditorObj from './index_pattern_field_editor.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 31 | 1 | 29 | 4 | + ## Client ### Start diff --git a/api_docs/index_pattern_management.mdx b/api_docs/index_pattern_management.mdx index b74d3a7262870a..7d82efb213ff1c 100644 --- a/api_docs/index_pattern_management.mdx +++ b/api_docs/index_pattern_management.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import indexPatternManagementObj from './index_pattern_management.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 46 | 0 | 46 | 4 | + ## Client ### Setup diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 83eb9982628644..0a13dcdfb5bcca 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import infraObj from './infra.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 23 | 0 | 20 | 4 | + ## Client ### Objects diff --git a/api_docs/ingest_pipelines.mdx b/api_docs/ingest_pipelines.mdx index eb56280c5b4ae5..25bdbceda4eadd 100644 --- a/api_docs/ingest_pipelines.mdx +++ b/api_docs/ingest_pipelines.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import ingestPipelinesObj from './ingest_pipelines.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 9 | 0 | 9 | 4 | + ## Client ### Classes diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 1fe62ba63e26e3..d69ef22ce788ad 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import inspectorObj from './inspector.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 101 | 6 | 78 | 4 | + ## Client ### Setup diff --git a/api_docs/kibana_legacy.mdx b/api_docs/kibana_legacy.mdx index 208cb8013d063b..5cd9244f0ea57f 100644 --- a/api_docs/kibana_legacy.mdx +++ b/api_docs/kibana_legacy.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import kibanaLegacyObj from './kibana_legacy.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 93 | 3 | 85 | 1 | + ## Client ### Functions diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index f4a5cf71d6a3d8..da356108593a14 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import kibanaReactObj from './kibana_react.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 245 | 1 | 216 | 4 | + ## Client ### Objects diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 9a9001af4b1f0c..a76992e0bc2b89 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import kibanaUtilsObj from './kibana_utils.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 551 | 5 | 373 | 8 | + ## Client ### Objects diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 8e9369253c2e34..8c47be1b5897ca 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import lensObj from './lens.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 172 | 0 | 161 | 16 | + ## Client ### Interfaces diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index d11b92e9bbc145..a2859cc03f8775 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import licenseApiGuardObj from './license_api_guard.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 8 | 0 | 8 | 0 | + ## Server ### Classes diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index 8fde04390ebf08..b198e83acebf35 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import licenseManagementObj from './license_management.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 3 | 0 | + ## Client ### Setup diff --git a/api_docs/licensing.json b/api_docs/licensing.json index 6959c5fc0e5114..862a00e65215f4 100644 --- a/api_docs/licensing.json +++ b/api_docs/licensing.json @@ -3074,7 +3074,7 @@ "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 225 + "lineNumber": 229 } }, { diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index 695734b79f4934..af26b10762d273 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import licensingObj from './licensing.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 120 | 0 | 44 | 8 | + ## Client ### Setup diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 8ef77a85fb548e..47b1bde5c07822 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import listsObj from './lists.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 151 | 0 | 143 | 38 | + ## Client ### Setup diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 16e097a6158e6a..22586c7b58b494 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import managementObj from './management.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 38 | 0 | 38 | 4 | + ## Client ### Setup diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index f4f5594b193471..efa7ddb5fddf58 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import mapsObj from './maps.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 196 | 2 | 195 | 11 | + ## Client ### Start diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 5907add0c54659..0abd9d511cf90c 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import mapsEmsObj from './maps_ems.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 75 | 1 | 75 | 0 | + ## Client ### Setup diff --git a/api_docs/metrics_entities.mdx b/api_docs/metrics_entities.mdx index 19a27636511c3c..182a78cd0ddc1b 100644 --- a/api_docs/metrics_entities.mdx +++ b/api_docs/metrics_entities.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import metricsEntitiesObj from './metrics_entities.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 9 | 0 | 6 | 1 | + ## Server ### Setup diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index abb03511bbcaa7..4a9b98a3268373 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import mlObj from './ml.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 274 | 10 | 270 | 32 | + ## Client ### Start diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index 4c32b1c933c930..e3dd2a0bbb6a5d 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import monitoringObj from './monitoring.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 10 | 0 | 10 | 2 | + ## Server ### Setup diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index d9dc1703ed4d86..b8b6f82162916a 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import navigationObj from './navigation.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 31 | 0 | 31 | 2 | + ## Client ### Setup diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index cb5c264e1940c7..0c3fe2bbc6640a 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import newsfeedObj from './newsfeed.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 17 | 0 | 17 | 0 | + ## Client ### Setup diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 353e65b0fa0808..6787e3f0238c87 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import observabilityObj from './observability.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 194 | 0 | 194 | 10 | + ## Client ### Setup diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 8638e916107a4a..a981f8f4b8c658 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import osqueryObj from './osquery.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 10 | 0 | 10 | 0 | + ## Client ### Setup diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index 9416a74b8f42c1..3df30c54e4390b 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import presentationUtilObj from './presentation_util.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 125 | 1 | 121 | 3 | + ## Client ### Setup diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index a669a858ba6be1..166aeee82153e1 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import remoteClustersObj from './remote_clusters.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 4 | 0 | + ## Client ### Setup diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index 445d38e4ec71b8..f349b223ddf97c 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import reportingObj from './reporting.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 133 | 1 | 132 | 19 | + ## Client ### Start diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index 3216c8cd968952..7a7765dfe1584a 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import rollupObj from './rollup.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 20 | 0 | 20 | 0 | + ## Common ### Objects diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index 964d29594d3c66..a0171607e8eebd 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import ruleRegistryObj from './rule_registry.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 43 | 0 | 43 | 6 | + ## Server ### Setup diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index a1d1b548350eec..2b9585f558ad5a 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import runtimeFieldsObj from './runtime_fields.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 24 | 0 | 19 | 2 | + ## Client ### Setup diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index 01d7f9fce991fc..66579c3e3b0ca7 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import savedObjectsObj from './saved_objects.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 211 | 3 | 197 | 5 | + ## Client ### Setup diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index 916288f934b575..0ee0660609f14d 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import savedObjectsManagementObj from './saved_objects_management.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 96 | 0 | 85 | 0 | + ## Client ### Setup diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 52a8b19a8300bc..fe1d90171a2c17 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import savedObjectsTaggingObj from './saved_objects_tagging.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 54 | 0 | 50 | 0 | + ## Client ### Start diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 921ee5240bfe8b..6fe8b4c6d46981 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import savedObjectsTaggingOssObj from './saved_objects_tagging_oss.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 89 | 3 | 50 | 0 | + ## Client ### Setup diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index cdb70b22b0837c..a098e5f296d69d 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import screenshotModeObj from './screenshot_mode.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 21 | 0 | 16 | 1 | + ## Client ### Setup diff --git a/api_docs/security.json b/api_docs/security.json index e0fd2a6bb33f2e..f7547221cec666 100644 --- a/api_docs/security.json +++ b/api_docs/security.json @@ -14,7 +14,13 @@ "\nRepresents the currently authenticated user." ], "signature": [ - "AuthenticatedUser", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, " extends ", "User" ], @@ -120,7 +126,13 @@ ], "signature": [ "() => Promise<", - "AuthenticatedUser", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, ">" ], "source": { @@ -943,6 +955,121 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "security", + "id": "def-server.AuditServiceSetup", + "type": "Interface", + "tags": [], + "label": "AuditServiceSetup", + "description": [], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 40 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.AuditServiceSetup.asScoped", + "type": "Function", + "tags": [], + "label": "asScoped", + "description": [], + "signature": [ + "(request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") => ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuditLogger", + "text": "AuditLogger" + } + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 41 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "security", + "id": "def-server.request", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 41 + }, + "deprecated": false + } + ] + }, + { + "parentPluginId": "security", + "id": "def-server.AuditServiceSetup.getLogger", + "type": "Function", + "tags": [], + "label": "getLogger", + "description": [], + "signature": [ + "(id?: string | undefined) => ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.LegacyAuditLogger", + "text": "LegacyAuditLogger" + } + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 42 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "security", + "id": "def-server.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 42 + }, + "deprecated": false + } + ] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "security", "id": "def-server.AuthenticatedUser", @@ -953,7 +1080,13 @@ "\nRepresents the currently authenticated user." ], "signature": [ - "AuthenticatedUser", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, " extends ", "User" ], @@ -1035,6 +1168,174 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "security", + "id": "def-server.AuthenticationServiceStart", + "type": "Interface", + "tags": [], + "label": "AuthenticationServiceStart", + "description": [ + "\nAuthentication services available on the security plugin's start contract." + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 72 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.AuthenticationServiceStart.apiKeys", + "type": "Object", + "tags": [], + "label": "apiKeys", + "description": [], + "signature": [ + "{ create: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", params: ", + "CreateAPIKeyParams", + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.CreateAPIKeyResult", + "text": "CreateAPIKeyResult" + }, + " | null>; areAPIKeysEnabled: () => Promise; invalidate: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", params: ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeysParams", + "text": "InvalidateAPIKeysParams" + }, + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeyResult", + "text": "InvalidateAPIKeyResult" + }, + " | null>; grantAsInternalUser: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", createParams: ", + "CreateAPIKeyParams", + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.GrantAPIKeyResult", + "text": "GrantAPIKeyResult" + }, + " | null>; invalidateAsInternalUser: (params: ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeysParams", + "text": "InvalidateAPIKeysParams" + }, + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeyResult", + "text": "InvalidateAPIKeyResult" + }, + " | null>; }" + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 73 + }, + "deprecated": false + }, + { + "parentPluginId": "security", + "id": "def-server.AuthenticationServiceStart.getCurrentUser", + "type": "Function", + "tags": [], + "label": "getCurrentUser", + "description": [], + "signature": [ + "(request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") => ", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, + " | null" + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 81 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "security", + "id": "def-server.request", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 81 + }, + "deprecated": false + } + ] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "security", "id": "def-server.CheckPrivilegesPayload", @@ -1496,385 +1797,484 @@ } ], "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "security", + "id": "def-server.AuthorizationServiceSetup", + "type": "Type", + "tags": [], + "label": "AuthorizationServiceSetup", + "description": [], + "source": { + "path": "x-pack/plugins/security/server/index.ts", + "lineNumber": 31 + }, + "deprecated": false, + "initialIsOpen": false }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup", - "type": "Interface", + "id": "def-server.ROUTE_TAG_CAN_REDIRECT", + "type": "string", "tags": [], - "label": "SecurityPluginSetup", + "label": "ROUTE_TAG_CAN_REDIRECT", "description": [ - "\nDescribes public Security plugin contract returned at the `setup` stage." + "\nIf the route is marked with this tag Security can safely assume that the calling party that sends\nrequest to this route can handle redirect responses. It's particularly important if we want the\nspecific route to be able to initiate or participate in the authentication handshake that may\ninvolve redirects and will eventually redirect authenticated user to this route." + ], + "signature": [ + "\"security:canRedirect\"" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 69 + "path": "x-pack/plugins/security/server/routes/tags.ts", + "lineNumber": 21 }, "deprecated": false, - "children": [ - { - "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.authc", - "type": "Object", - "tags": [ - "deprecated" - ], - "label": "authc", - "description": [], - "signature": [ - "{ getCurrentUser: (request: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCoreHttpPluginApi", - "section": "def-server.KibanaRequest", - "text": "KibanaRequest" - }, - ") => ", - "AuthenticatedUser", - " | null; }" - ], - "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 73 + "initialIsOpen": false + } + ], + "objects": [], + "setup": { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup", + "type": "Interface", + "tags": [], + "label": "SecurityPluginSetup", + "description": [ + "\nDescribes public Security plugin contract returned at the `setup` stage." + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 72 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.authc", + "type": "Object", + "tags": [ + "deprecated" + ], + "label": "authc", + "description": [], + "signature": [ + "{ getCurrentUser: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" }, - "deprecated": true, - "references": [ - { - "plugin": "reporting", - "link": { - "path": "x-pack/plugins/reporting/server/routes/lib/get_user.ts", - "lineNumber": 13 - } - }, - { - "plugin": "encryptedSavedObjects", - "link": { - "path": "x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts", - "lineNumber": 107 - } - }, - { - "plugin": "encryptedSavedObjects", - "link": { - "path": "x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts", - "lineNumber": 67 - } - }, - { - "plugin": "actions", - "link": { - "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 444 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/routes/annotations.ts", - "lineNumber": 105 - } - }, - { - "plugin": "dashboardMode", - "link": { - "path": "x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts", - "lineNumber": 33 - } - }, - { - "plugin": "dataEnhanced", - "link": { - "path": "x-pack/plugins/data_enhanced/server/search/session/session_service.ts", - "lineNumber": 448 - } - }, - { - "plugin": "logstash", - "link": { - "path": "x-pack/plugins/logstash/server/routes/pipeline/save.ts", - "lineNumber": 41 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts", - "lineNumber": 48 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts", - "lineNumber": 45 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts", - "lineNumber": 45 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts", - "lineNumber": 28 - } + ") => ", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, + " | null; }" + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 76 + }, + "deprecated": true, + "references": [ + { + "plugin": "reporting", + "link": { + "path": "x-pack/plugins/reporting/server/routes/lib/get_user.ts", + "lineNumber": 13 } - ] + }, + { + "plugin": "encryptedSavedObjects", + "link": { + "path": "x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts", + "lineNumber": 107 + } + }, + { + "plugin": "encryptedSavedObjects", + "link": { + "path": "x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts", + "lineNumber": 67 + } + }, + { + "plugin": "actions", + "link": { + "path": "x-pack/plugins/actions/server/plugin.ts", + "lineNumber": 444 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/routes/annotations.ts", + "lineNumber": 105 + } + }, + { + "plugin": "dashboardMode", + "link": { + "path": "x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts", + "lineNumber": 33 + } + }, + { + "plugin": "dataEnhanced", + "link": { + "path": "x-pack/plugins/data_enhanced/server/search/session/session_service.ts", + "lineNumber": 448 + } + }, + { + "plugin": "logstash", + "link": { + "path": "x-pack/plugins/logstash/server/routes/pipeline/save.ts", + "lineNumber": 41 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts", + "lineNumber": 48 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts", + "lineNumber": 45 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts", + "lineNumber": 45 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts", + "lineNumber": 28 + } + } + ] + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.authz", + "type": "Object", + "tags": [ + "deprecated" + ], + "label": "authz", + "description": [], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuthorizationServiceSetup", + "text": "AuthorizationServiceSetup" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 80 }, - { - "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.authz", - "type": "Object", - "tags": [ - "deprecated" - ], - "label": "authz", - "description": [], - "signature": [ - "{ mode: ", - "AuthorizationMode", - "; actions: ", - "Actions", - "; checkPrivilegesDynamicallyWithRequest: ", - "CheckPrivilegesDynamicallyWithRequest", - "; checkPrivilegesWithRequest: ", - "CheckPrivilegesWithRequest", - "; }" - ], - "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 77 + "deprecated": true, + "references": [ + { + "plugin": "actions", + "link": { + "path": "x-pack/plugins/actions/server/plugin.ts", + "lineNumber": 443 + } }, - "deprecated": true, - "references": [ - { - "plugin": "actions", - "link": { - "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 443 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts", - "lineNumber": 54 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/plugin.ts", - "lineNumber": 153 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/plugin.ts", - "lineNumber": 203 - } - }, - { - "plugin": "enterpriseSearch", - "link": { - "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", - "lineNumber": 46 - } - }, - { - "plugin": "enterpriseSearch", - "link": { - "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", - "lineNumber": 86 - } - }, - { - "plugin": "enterpriseSearch", - "link": { - "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", - "lineNumber": 88 - } - }, - { - "plugin": "savedObjectsTagging", - "link": { - "path": "x-pack/plugins/saved_objects_tagging/server/request_handler_context.ts", - "lineNumber": 37 - } + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts", + "lineNumber": 54 } - ] + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/plugin.ts", + "lineNumber": 153 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/plugin.ts", + "lineNumber": 203 + } + }, + { + "plugin": "enterpriseSearch", + "link": { + "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", + "lineNumber": 46 + } + }, + { + "plugin": "enterpriseSearch", + "link": { + "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", + "lineNumber": 86 + } + }, + { + "plugin": "enterpriseSearch", + "link": { + "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", + "lineNumber": 88 + } + }, + { + "plugin": "savedObjectsTagging", + "link": { + "path": "x-pack/plugins/saved_objects_tagging/server/request_handler_context.ts", + "lineNumber": 37 + } + } + ] + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.license", + "type": "Object", + "tags": [], + "label": "license", + "description": [ + "\nExposes information about the available security features under the current license." + ], + "signature": [ + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.SecurityLicense", + "text": "SecurityLicense" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 84 }, + "deprecated": false + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.audit", + "type": "Object", + "tags": [], + "label": "audit", + "description": [ + "\nExposes services for audit logging." + ], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuditServiceSetup", + "text": "AuditServiceSetup" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 88 + }, + "deprecated": false + } + ], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "security", + "id": "def-server.SecurityPluginStart", + "type": "Interface", + "tags": [], + "label": "SecurityPluginStart", + "description": [ + "\nDescribes public Security plugin contract returned at the `start` stage." + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 94 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginStart.authc", + "type": "Object", + "tags": [], + "label": "authc", + "description": [ + "\nAuthentication services to confirm the user is who they say they are." + ], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuthenticationServiceStart", + "text": "AuthenticationServiceStart" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 98 + }, + "deprecated": false + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginStart.authz", + "type": "Object", + "tags": [], + "label": "authz", + "description": [ + "\nAuthorization services to manage and access the permissions a particular user has." + ], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuthorizationServiceSetup", + "text": "AuthorizationServiceSetup" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 102 + }, + "deprecated": false + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "security", + "id": "def-common.AuthenticatedUser", + "type": "Interface", + "tags": [], + "label": "AuthenticatedUser", + "description": [ + "\nRepresents the currently authenticated user." + ], + "signature": [ + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, + " extends ", + "User" + ], + "source": { + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 21 + }, + "deprecated": false, + "children": [ { "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.license", + "id": "def-common.AuthenticatedUser.authentication_realm", "type": "Object", "tags": [], - "label": "license", - "description": [], + "label": "authentication_realm", + "description": [ + "\nThe name and type of the Realm that has authenticated the user." + ], "signature": [ - { - "pluginId": "security", - "scope": "common", - "docId": "kibSecurityPluginApi", - "section": "def-common.SecurityLicense", - "text": "SecurityLicense" - } + "UserRealm" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 81 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 25 }, "deprecated": false }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.audit", + "id": "def-common.AuthenticatedUser.lookup_realm", "type": "Object", "tags": [], - "label": "audit", - "description": [], + "label": "lookup_realm", + "description": [ + "\nThe name and type of the Realm where the user information were retrieved from." + ], "signature": [ - "AuditServiceSetup" + "UserRealm" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 82 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 30 }, "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "security", - "id": "def-server.SecurityPluginStart", - "type": "Interface", - "tags": [], - "label": "SecurityPluginStart", - "description": [ - "\nDescribes public Security plugin contract returned at the `start` stage." - ], - "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 88 - }, - "deprecated": false, - "children": [ + }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginStart.authc", + "id": "def-common.AuthenticatedUser.authentication_provider", "type": "Object", "tags": [], - "label": "authc", - "description": [], + "label": "authentication_provider", + "description": [ + "\nThe authentication provider that used to authenticate user." + ], "signature": [ - "{ getCurrentUser: (request: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCoreHttpPluginApi", - "section": "def-server.KibanaRequest", - "text": "KibanaRequest" - }, - ") => ", - "AuthenticatedUser", - " | null; apiKeys: Pick<", - "APIKeys", - ", \"create\" | \"areAPIKeysEnabled\" | \"invalidate\" | \"grantAsInternalUser\" | \"invalidateAsInternalUser\">; }" + "AuthenticationProvider" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 89 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 35 }, "deprecated": false }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginStart.authz", - "type": "Object", + "id": "def-common.AuthenticatedUser.authentication_type", + "type": "string", "tags": [], - "label": "authz", - "description": [], - "signature": [ - "{ mode: ", - "AuthorizationMode", - "; actions: ", - "Actions", - "; checkPrivilegesDynamicallyWithRequest: ", - "CheckPrivilegesDynamicallyWithRequest", - "; checkPrivilegesWithRequest: ", - "CheckPrivilegesWithRequest", - "; }" + "label": "authentication_type", + "description": [ + "\nThe AuthenticationType used by ES to authenticate the user.\n" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 90 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 42 }, "deprecated": false } ], "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "security", - "id": "def-server.AuthorizationServiceSetup", - "type": "Type", - "tags": [], - "label": "AuthorizationServiceSetup", - "description": [], - "signature": [ - "{ mode: ", - "AuthorizationMode", - "; actions: ", - "Actions", - "; checkPrivilegesDynamicallyWithRequest: ", - "CheckPrivilegesDynamicallyWithRequest", - "; checkPrivilegesWithRequest: ", - "CheckPrivilegesWithRequest", - "; }" - ], - "source": { - "path": "x-pack/plugins/security/server/index.ts", - "lineNumber": 30 - }, - "deprecated": false, - "initialIsOpen": false }, - { - "parentPluginId": "security", - "id": "def-server.ROUTE_TAG_CAN_REDIRECT", - "type": "string", - "tags": [], - "label": "ROUTE_TAG_CAN_REDIRECT", - "description": [ - "\nIf the route is marked with this tag Security can safely assume that the calling party that sends\nrequest to this route can handle redirect responses. It's particularly important if we want the\nspecific route to be able to initiate or participate in the authentication handshake that may\ninvolve redirects and will eventually redirect authenticated user to this route." - ], - "signature": [ - "\"security:canRedirect\"" - ], - "source": { - "path": "x-pack/plugins/security/server/routes/tags.ts", - "lineNumber": 21 - }, - "deprecated": false, - "initialIsOpen": false - } - ], - "objects": [] - }, - "common": { - "classes": [], - "functions": [], - "interfaces": [ { "parentPluginId": "security", "id": "def-common.SecurityLicense", diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 07f219ce6e7715..e31fa44014c941 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import securityObj from './security.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 95 | 0 | 45 | 11 | + ## Client ### Setup @@ -27,6 +37,12 @@ import securityObj from './security.json'; ## Server +### Setup + + +### Start + + ### Interfaces diff --git a/api_docs/security_oss.mdx b/api_docs/security_oss.mdx index f8c49a56551cd3..ada97232ffe846 100644 --- a/api_docs/security_oss.mdx +++ b/api_docs/security_oss.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import securityOssObj from './security_oss.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 12 | 0 | 9 | 3 | + ## Client ### Setup diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index b38a877f7edd56..0667a98e3762bf 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import securitySolutionObj from './security_solution.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 32 | 0 | 32 | 6 | + ## Client ### Setup diff --git a/api_docs/share.mdx b/api_docs/share.mdx index e2a9da26c0fc46..58886148bd4a7b 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import shareObj from './share.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 67 | 1 | 61 | 4 | + ## Client ### Setup diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index a0fc3231baf4d9..ab2f9175987378 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import snapshotRestoreObj from './snapshot_restore.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 22 | 1 | 22 | 1 | + ## Common ### Objects diff --git a/api_docs/spaces.json b/api_docs/spaces.json index d24b3884ffac6f..002145f3b48db6 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1203,7 +1203,7 @@ "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 263 + "lineNumber": 267 } }, { @@ -2002,28 +2002,28 @@ "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 57 + "lineNumber": 60 } }, { "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 263 + "lineNumber": 267 } }, { "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 281 + "lineNumber": 285 } }, { "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 297 + "lineNumber": 301 } }, { diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index ccaffdd3748b55..197fe460b47d35 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import spacesObj from './spaces.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 96 | 0 | 0 | 0 | + ## Client ### Setup diff --git a/api_docs/spaces_oss.mdx b/api_docs/spaces_oss.mdx index e889a319f5f23c..b36b0b4c19aff4 100644 --- a/api_docs/spaces_oss.mdx +++ b/api_docs/spaces_oss.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import spacesOssObj from './spaces_oss.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 71 | 0 | 5 | 0 | + ## Client ### Setup diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index 9fbed513491dd0..eefe0a2dc76fe7 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import stackAlertsObj from './stack_alerts.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 4 | 0 | + ## Server ### Consts, variables and types diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index b143323bf1c7a5..5c6d5b3d08ba48 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import taskManagerObj from './task_manager.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 44 | 0 | 18 | 7 | + ## Server ### Setup diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index 995c9b22e268ab..f8d632c0d97082 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import telemetryObj from './telemetry.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 42 | 0 | 0 | 0 | + ## Client ### Setup diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index d112897e1c4899..749ec3ba2bd71e 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import telemetryCollectionManagerObj from './telemetry_collection_manager.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 29 | 0 | 29 | 4 | + ## Server ### Setup diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index 058a9d3fcb460e..53914260b037f3 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import telemetryCollectionXpackObj from './telemetry_collection_xpack.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 1 | 0 | 1 | 0 | + ## Server ### Consts, variables and types diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 78193e3485c591..71831a66284fc5 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import telemetryManagementSectionObj from './telemetry_management_section.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 14 | 0 | 13 | 0 | + ## Client ### Setup diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index cb16e39419a438..e9d4a75e39991a 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import timelinesObj from './timelines.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 6 | 0 | 6 | 1 | + ## Client ### Setup diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 50a7a11abdf16c..91327ad1f990f3 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import triggersActionsUiObj from './triggers_actions_ui.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 237 | 1 | 228 | 19 | + ## Client ### Setup diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 9833f2a19e63ac..9045dc736bb2f8 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import uiActionsObj from './ui_actions.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 127 | 0 | 88 | 11 | + ## Client ### Setup diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index 285232bf3fe0cd..3e9c9247532290 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import uiActionsEnhancedObj from './ui_actions_enhanced.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 205 | 2 | 147 | 10 | + ## Client ### Setup diff --git a/api_docs/uptime.mdx b/api_docs/uptime.mdx index 8d63edcd18ecad..ceb7d854a8fecc 100644 --- a/api_docs/uptime.mdx +++ b/api_docs/uptime.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import uptimeObj from './uptime.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 6 | 0 | 6 | 3 | + ## Server ### Functions diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index a80a1e0e53f277..b457ca9eb331fa 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import urlForwardingObj from './url_forwarding.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 15 | 0 | 15 | 0 | + ## Client ### Classes diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 62db0389c2d5cd..197cfc72f17afb 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import usageCollectionObj from './usage_collection.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 57 | 0 | 16 | 2 | + ## Client ### Setup diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 041d01196bad9d..a904e00ef0efc5 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import visTypeTimeseriesObj from './vis_type_timeseries.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 10 | 1 | 10 | 3 | + ## Server ### Setup diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index d3834b2d5f83f8..c6d9bdc741d4af 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -11,6 +11,16 @@ warning: This document is auto-generated and is meant to be viewed inside our ex import visualizationsObj from './visualizations.json'; + + + + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 246 | 13 | 228 | 12 | + ## Client ### Setup diff --git a/docs/api/index-patterns.asciidoc b/docs/api/index-patterns.asciidoc index 79d2f164fb8c3b..b343d80b4fc15c 100644 --- a/docs/api/index-patterns.asciidoc +++ b/docs/api/index-patterns.asciidoc @@ -20,6 +20,12 @@ The following index patterns APIs are available: ** <> to set a default index pattern * Fields ** <> to change field metadata, such as `count`, `customLabel` and `format` +* Runtime fields + ** <> to retrieve a runtime field + ** <> to create a runtime field + ** <> to create or update a runtime field + ** <> to partially update an existing runtime field + ** <> to delete a runtime field include::index-patterns/get.asciidoc[] include::index-patterns/create.asciidoc[] @@ -28,3 +34,9 @@ include::index-patterns/delete.asciidoc[] include::index-patterns/default-get.asciidoc[] include::index-patterns/default-set.asciidoc[] include::index-patterns/update-fields.asciidoc[] +include::index-patterns/runtime-fields/get.asciidoc[] +include::index-patterns/runtime-fields/create.asciidoc[] +include::index-patterns/runtime-fields/upsert.asciidoc[] +include::index-patterns/runtime-fields/update.asciidoc[] +include::index-patterns/runtime-fields/delete.asciidoc[] + diff --git a/docs/api/index-patterns/create.asciidoc b/docs/api/index-patterns/create.asciidoc index 771292d6f934d5..521e25931ad49f 100644 --- a/docs/api/index-patterns/create.asciidoc +++ b/docs/api/index-patterns/create.asciidoc @@ -84,6 +84,7 @@ $ curl -X POST api/index_patterns/index_pattern "typeMeta": {}, "fieldFormats": {}, "fieldAttrs": {}, + "runtimeFieldMap": {} "allowNoIndex": "..." } } diff --git a/docs/api/index-patterns/get.asciidoc b/docs/api/index-patterns/get.asciidoc index 3f53bf0726bf14..64588e63f62ae3 100644 --- a/docs/api/index-patterns/get.asciidoc +++ b/docs/api/index-patterns/get.asciidoc @@ -58,6 +58,7 @@ The API returns an index pattern object: "typeMeta": {}, "fieldFormats": {}, "fieldAttrs": {}, + "runtimeFieldMap" {}, "allowNoIndex: "..." } } diff --git a/docs/api/index-patterns/runtime-fields/create.asciidoc b/docs/api/index-patterns/runtime-fields/create.asciidoc new file mode 100644 index 00000000000000..b0773c29e5309f --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/create.asciidoc @@ -0,0 +1,61 @@ +[[index-patterns-runtime-field-api-create]] +=== Create runtime field API +++++ +Create runtime field +++++ + +experimental[] Create a runtime field + +[[index-patterns-runtime-field-create-request]] +==== Request + +`POST :/api/index_patterns/index_pattern//runtime_field` + +`POST :/s//api/index_patterns/index_pattern//runtime_field` + +[[index-patterns-runtime-field-create-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +[[index-patterns-runtime-field-create-body]] +==== Request body + +`name`:: (Required, string) The name for a runtime field. + +`runtimeField`:: (Required, object) The runtime field definition object. + + +[[index-patterns-runtime-field-create-example]] +==== Examples + +Create a runtime field on an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern//runtime_field +{ + "name": "runtimeFoo", + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc["foo"].value)" + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns created runtime field object and update index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...}, + "field": {...} +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/runtime-fields/delete.asciidoc b/docs/api/index-patterns/runtime-fields/delete.asciidoc new file mode 100644 index 00000000000000..840789fe1ec23f --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/delete.asciidoc @@ -0,0 +1,37 @@ +[[index-patterns-runtime-field-api-delete]] +=== Delete runtime field API +++++ +Delete runtime field +++++ + +experimental[] Delete a runtime field from an index pattern. + +[[index-patterns-runtime-field-api-delete-request]] +==== Request + +`DELETE :/api/index_patterns/index_pattern//runtime_field/` + +`DELETE :/s//api/index_patterns/index_pattern//runtime_field/` + +[[index-patterns-runtime-field-api-delete-path-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern your want to delete a runtime field from. + +`name`:: +(Required, string) The name of the runtime field you want to delete. + + +==== Example + +Delete a runtime field from an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/index_patterns/index_pattern//runtime_field/ +-------------------------------------------------- +// KIBANA diff --git a/docs/api/index-patterns/runtime-fields/get.asciidoc b/docs/api/index-patterns/runtime-fields/get.asciidoc new file mode 100644 index 00000000000000..42bd209c708bc3 --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/get.asciidoc @@ -0,0 +1,52 @@ +[[index-patterns-runtime-field-api-get]] +=== Get runtime field API +++++ +Get runtime field +++++ + +experimental[] Get a runtime field + +[[index-patterns-runtime-field-get-request]] +==== Request + +`GET :/api/index_patterns/index_pattern//runtime_field/` + +`GET :/s//api/index_patterns/index_pattern//runtime_field/` + +[[index-patterns-runtime-field-get-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +`name`:: +(Required, string) The name of the runtime field you want to retrieve. + + +[[index-patterns-runtime-field-get-example]] +==== Example + +Retrieve a runtime field named `foo` of index pattern with the `my-pattern` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/index_patterns/index_pattern/my-pattern/runtime_field/foo +-------------------------------------------------- +// KIBANA + +The API returns a runtime `field` object, and a `runtimeField` definition object: + +[source,sh] +-------------------------------------------------- +{ + "field": { + ... + }, + "runtimeField": { + ... + } +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/runtime-fields/update.asciidoc b/docs/api/index-patterns/runtime-fields/update.asciidoc new file mode 100644 index 00000000000000..f34460896f7bc9 --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/update.asciidoc @@ -0,0 +1,66 @@ +[[index-patterns-runtime-field-api-update]] +=== Update runtime field API +++++ +Update runtime field +++++ + +experimental[] Update an existing runtime field + +[[index-patterns-runtime-field-update-request]] +==== Request + +`POST :/api/index_patterns/index_pattern//runtime_field/` + +`POST :/s//api/index_patterns/index_pattern//runtime_field/` + +[[index-patterns-runtime-field-update-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +`name`:: +(Required, string) The name of the runtime field you want to update. + +[[index-patterns-runtime-field-update-body]] +==== Request body + +`runtimeField`:: (Required, object) The runtime field definition object. + +You can update following fields: + +* `type` +* `script` + + + +[[index-patterns-runtime-field-update-example]] +==== Examples + +Update an existing runtime field on an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern//runtime_field/ +{ + "runtimeField": { + "script": { + "source": "emit(doc["bar"].value)" + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns updated runtime field object and updated index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...}, + "field": {...} +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/runtime-fields/upsert.asciidoc b/docs/api/index-patterns/runtime-fields/upsert.asciidoc new file mode 100644 index 00000000000000..1b436db19c62e4 --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/upsert.asciidoc @@ -0,0 +1,61 @@ +[[index-patterns-runtime-field-api-upsert]] +=== Upsert runtime field API +++++ +Upsert runtime field +++++ + +experimental[] Create or update an existing runtime field + +[[index-patterns-runtime-field-upsert-request]] +==== Request + +`PUT :/api/index_patterns/index_pattern//runtime_field` + +`PUT :/s//api/index_patterns/index_pattern//runtime_field` + +[[index-patterns-runtime-field-upsert-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +[[index-patterns-runtime-field-upsert-body]] +==== Request body + +`name`:: (Required, string) The name for a new runtime field or a name of an existing runtime field. + +`runtimeField`:: (Required, object) The runtime field definition object. + + +[[index-patterns-runtime-field-upsert-example]] +==== Examples + +Create or update an existing runtime field on an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/index_patterns/index_pattern//runtime_field +{ + "name": "runtimeFoo", + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc["foo"].value)" + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns created or updated runtime field object and updated index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...}, + "field": {...} +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/update.asciidoc b/docs/api/index-patterns/update.asciidoc index 8ed0ff89fb928c..2d5fe882d448df 100644 --- a/docs/api/index-patterns/update.asciidoc +++ b/docs/api/index-patterns/update.asciidoc @@ -93,7 +93,8 @@ $ curl -X POST api/saved_objects/index-pattern/my-pattern "fieldFormats": {}, "type": "...", "typeMeta": {}, - "fields": {} + "fields": {}, + "runtimeFieldMap": {} } } -------------------------------------------------- diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 029ee9ea4faf6c..0ae806618adc5d 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -97,6 +97,7 @@ yarn kbn watch-bazel - @kbn/securitysolution-list-utils - @kbn/securitysolution-utils - @kbn/server-http-tools +- @kbn/server-route-repository - @kbn/std - @kbn/telemetry-utils - @kbn/tinymath diff --git a/docs/developer/getting-started/sample-data.asciidoc b/docs/developer/getting-started/sample-data.asciidoc index 0d313cbabe64e4..2454c9d8a61468 100644 --- a/docs/developer/getting-started/sample-data.asciidoc +++ b/docs/developer/getting-started/sample-data.asciidoc @@ -6,7 +6,7 @@ There are a couple ways to easily get data ingested into {es}. [discrete] === Sample data packages available for one click installation -The easiest is to install one or more of our vailable sample data packages. If you have no data, you should be +The easiest is to install one or more of our available sample data packages. If you have no data, you should be prompted to install when running {kib} for the first time. You can also access and install the sample data packages by going to the home page and clicking "add sample data". @@ -27,5 +27,5 @@ Make sure to execute `node scripts/makelogs` *after* {es} is up and running! [discrete] === CSV upload -If running with a platinum or trial license, you can also use the CSV uploader provided inside the Machine learning app. -Navigate to the Data visualizer to upload your data from a file. \ No newline at end of file +You can also use the CSV uploader provided on the {kib} home page. +Navigate to **Add data** > **Upload file** to upload your data from a file. \ No newline at end of file diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.description.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.description.md new file mode 100644 index 00000000000000..b6bba3b5e356cd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.description.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [description](./kibana-plugin-core-server.pluginmanifest.description.md) + +## PluginManifest.description property + +TODO: make required once all plugins specify this. A brief description of what this plugin does and any capabilities it provides. + +Signature: + +```typescript +readonly description?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md index bd15b95d73acea..b3e20bc7ed693a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md @@ -21,10 +21,12 @@ Should never be used in code outside of Core but is exported for documentation p | Property | Type | Description | | --- | --- | --- | | [configPath](./kibana-plugin-core-server.pluginmanifest.configpath.md) | ConfigPath | Root used by the plugin, defaults to "id" in snake\_case format. | +| [description](./kibana-plugin-core-server.pluginmanifest.description.md) | string | TODO: make required once all plugins specify this. A brief description of what this plugin does and any capabilities it provides. | | [extraPublicDirs](./kibana-plugin-core-server.pluginmanifest.extrapublicdirs.md) | string[] | Specifies directory names that can be imported by other ui-plugins built using the same instance of the @kbn/optimizer. A temporary measure we plan to replace with better mechanisms for sharing static code between plugins | | [id](./kibana-plugin-core-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. | | [kibanaVersion](./kibana-plugin-core-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | | [optionalPlugins](./kibana-plugin-core-server.pluginmanifest.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | +| [owner](./kibana-plugin-core-server.pluginmanifest.owner.md) | {
readonly name: string;
readonly githubTeam?: string;
} | TODO: make required once all internal plugins have this specified. | | [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | readonly string[] | List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins. | | [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | | [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean | Specifies whether plugin includes some server-side specific functionality. | diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.owner.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.owner.md new file mode 100644 index 00000000000000..a90af81aa186a4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.owner.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [owner](./kibana-plugin-core-server.pluginmanifest.owner.md) + +## PluginManifest.owner property + +TODO: make required once all internal plugins have this specified. + +Signature: + +```typescript +readonly owner?: { + readonly name: string; + readonly githubTeam?: string; + }; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md new file mode 100644 index 00000000000000..c0aca53255b8fd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md) + +## IndexPattern.getRuntimeField() method + +Returns runtime field if exists + +Signature: + +```typescript +getRuntimeField(name: string): RuntimeField | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`RuntimeField | null` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md new file mode 100644 index 00000000000000..96dbe13a7f1978 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [hasRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md) + +## IndexPattern.hasRuntimeField() method + +Checks if runtime field exists + +Signature: + +```typescript +hasRuntimeField(name: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 53d173d39f50d0..51ca42fdce70a7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -54,13 +54,16 @@ export declare class IndexPattern implements IIndexPattern | [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists | | [getNonScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md) | | | +| [getRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md) | | Returns runtime field if exists | | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | +| [hasRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md) | | Checks if runtime field exists | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | -| [removeRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [replaceAllRuntimeFields(newFields)](./kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md) | | Replaces all existing runtime fields with new fields | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) | | | | [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md index 7a5228fece782e..f2774924fc73c1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md @@ -4,7 +4,7 @@ ## IndexPattern.removeRuntimeField() method -Remove a runtime field - removed from mapped field or removed unmapped field as appropriate +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. Signature: @@ -16,7 +16,7 @@ removeRuntimeField(name: string): void; | Parameter | Type | Description | | --- | --- | --- | -| name | string | | +| name | string | Field name to remove | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md new file mode 100644 index 00000000000000..076b2b38cf474a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [replaceAllRuntimeFields](./kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md) + +## IndexPattern.replaceAllRuntimeFields() method + +Replaces all existing runtime fields with new fields + +Signature: + +```typescript +replaceAllRuntimeFields(newFields: Record): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newFields | Record<string, RuntimeField> | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md new file mode 100644 index 00000000000000..d5dc8f966316b0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md) + +## IndexPattern.getRuntimeField() method + +Returns runtime field if exists + +Signature: + +```typescript +getRuntimeField(name: string): RuntimeField | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`RuntimeField | null` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md new file mode 100644 index 00000000000000..5000d5e645cbb9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [hasRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md) + +## IndexPattern.hasRuntimeField() method + +Checks if runtime field exists + +Signature: + +```typescript +hasRuntimeField(name: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 97d1cd91152623..27b8a31a2582ba 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -54,13 +54,16 @@ export declare class IndexPattern implements IIndexPattern | [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists | | [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | | +| [getRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md) | | Returns runtime field if exists | | [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | +| [hasRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md) | | Checks if runtime field exists | | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | -| [removeRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [replaceAllRuntimeFields(newFields)](./kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md) | | Replaces all existing runtime fields with new fields | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) | | | | [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md index da8e7e40a7fac2..ef32b80ba8502e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md @@ -4,7 +4,7 @@ ## IndexPattern.removeRuntimeField() method -Remove a runtime field - removed from mapped field or removed unmapped field as appropriate +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. Signature: @@ -16,7 +16,7 @@ removeRuntimeField(name: string): void; | Parameter | Type | Description | | --- | --- | --- | -| name | string | | +| name | string | Field name to remove | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md new file mode 100644 index 00000000000000..35df871763f8a1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [replaceAllRuntimeFields](./kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md) + +## IndexPattern.replaceAllRuntimeFields() method + +Replaces all existing runtime fields with new fields + +Signature: + +```typescript +replaceAllRuntimeFields(newFields: Record): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newFields | Record<string, RuntimeField> | | + +Returns: + +`void` + diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 8c0aa12ffc4c61..5e18d934863aa9 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -46,8 +46,7 @@ image::images/add-data-fleet.png[Add data using Fleet] [[upload-data-kibana]] === Upload a file -experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the File -Data Visualizer. You can upload a file up to 100 MB. This value is configurable up to 1 GB in +experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the {file-data-viz}. You can upload a file up to 100 MB. This value is configurable up to 1 GB in <>. To upload a file with geospatial data, refer to <>. diff --git a/docs/setup/images/add-data-fv.png b/docs/setup/images/add-data-fv.png old mode 100755 new mode 100644 index 45313d133822c1..7e253cdd0229d7 Binary files a/docs/setup/images/add-data-fv.png and b/docs/setup/images/add-data-fv.png differ diff --git a/docs/setup/images/add-data-tutorials.png b/docs/setup/images/add-data-tutorials.png index 74deedc57b42ed..782b44e383772c 100644 Binary files a/docs/setup/images/add-data-tutorials.png and b/docs/setup/images/add-data-tutorials.png differ diff --git a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc index f64c120f61298f..d6b90a4f19e112 100644 --- a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc +++ b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc @@ -6,6 +6,8 @@ Health monitoring ++++ +experimental[] + The Task Manager has an internal monitoring mechanism to keep track of a variety of metrics, which can be consumed with either the health monitoring API or the {kib} server log. The health monitoring API provides a reliable endpoint that can be monitored. diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc index 5e75aef0d9570f..c6a7b7f3d53fdc 100644 --- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc +++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc @@ -60,6 +60,8 @@ For details on scaling Task Manager, see <>. [[task-manager-diagnosing-root-cause]] ==== Diagnose a root cause for drift +experimental[] + The following guide helps you identify a root cause for _drift_ by making sense of the output from the <> endpoint. By analyzing the different sections of the output, you can evaluate different theories that explain the drift in a deployment. diff --git a/examples/index_pattern_field_editor_example/README.md b/examples/index_pattern_field_editor_example/README.md new file mode 100644 index 00000000000000..35ae814fc10e25 --- /dev/null +++ b/examples/index_pattern_field_editor_example/README.md @@ -0,0 +1,7 @@ +## index pattern field editor example + +This example index pattern field editor app shows how to: + - Edit index pattern fields via flyout + - Delete index pattern runtime fields with modal confirm prompt + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/index_pattern_field_editor_example/kibana.json b/examples/index_pattern_field_editor_example/kibana.json new file mode 100644 index 00000000000000..c522e6698ac3d6 --- /dev/null +++ b/examples/index_pattern_field_editor_example/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "indexPatternFieldEditorExample", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["data", "indexPatternFieldEditor", "developerExamples"], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/examples/index_pattern_field_editor_example/public/app.tsx b/examples/index_pattern_field_editor_example/public/app.tsx new file mode 100644 index 00000000000000..bd725759380aae --- /dev/null +++ b/examples/index_pattern_field_editor_example/public/app.tsx @@ -0,0 +1,145 @@ +/* + * 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 React, { useState } from 'react'; +import ReactDOM from 'react-dom'; +import { + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiButton, + EuiInMemoryTable, + EuiText, + DefaultItemAction, +} from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { + DataPublicPluginStart, + IndexPattern, + IndexPatternField, +} from '../../../src/plugins/data/public'; +import { IndexPatternFieldEditorStart } from '../../../src/plugins/index_pattern_field_editor/public'; + +interface Props { + indexPattern?: IndexPattern; + indexPatternFieldEditor: IndexPatternFieldEditorStart; +} + +const IndexPatternFieldEditorExample = ({ indexPattern, indexPatternFieldEditor }: Props) => { + const [fields, setFields] = useState( + indexPattern?.getNonScriptedFields() || [] + ); + const refreshFields = () => setFields(indexPattern?.getNonScriptedFields() || []); + const columns = [ + { + field: 'name', + name: 'Field name', + }, + { + name: 'Actions', + actions: [ + { + name: 'Edit', + description: 'Edit this field', + icon: 'pencil', + type: 'icon', + 'data-test-subj': 'editField', + onClick: (fld: IndexPatternField) => + indexPatternFieldEditor.openEditor({ + ctx: { indexPattern: indexPattern! }, + fieldName: fld.name, + onSave: refreshFields, + }), + }, + { + name: 'Delete', + description: 'Delete this field', + icon: 'trash', + type: 'icon', + 'data-test-subj': 'deleteField', + available: (fld) => !!fld.runtimeField, + onClick: (fld: IndexPatternField) => + indexPatternFieldEditor.openDeleteModal({ + fieldName: fld.name, + ctx: { + indexPattern: indexPattern!, + }, + onDelete: refreshFields, + }), + }, + ] as Array>, + }, + ]; + + const content = indexPattern ? ( + <> + Index pattern: {indexPattern?.title} +
+ + indexPatternFieldEditor.openEditor({ + ctx: { indexPattern: indexPattern! }, + onSave: refreshFields, + }) + } + data-test-subj="addField" + > + Add field + +
+ + items={fields} + columns={columns} + pagination={true} + hasActions={true} + sorting={{ + sort: { + field: 'name', + direction: 'asc', + }, + }} + /> + + ) : ( +

Please create an index pattern

+ ); + + return ( + + + Index pattern field editor demo + + {content} + + + + ); +}; + +interface RenderAppDependencies { + data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; +} + +export const renderApp = async ( + { data, indexPatternFieldEditor }: RenderAppDependencies, + { element }: AppMountParameters +) => { + const indexPattern = (await data.indexPatterns.getDefault()) || undefined; + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/index_pattern_management/public/service/environment/index.ts b/examples/index_pattern_field_editor_example/public/index.ts similarity index 74% rename from src/plugins/index_pattern_management/public/service/environment/index.ts rename to examples/index_pattern_field_editor_example/public/index.ts index ab5297ed0e14c0..cc509da31d25fb 100644 --- a/src/plugins/index_pattern_management/public/service/environment/index.ts +++ b/examples/index_pattern_field_editor_example/public/index.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -export { EnvironmentService, Environment, EnvironmentServiceSetup } from './environment'; +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export const plugin = () => new IndexPatternFieldEditorPlugin(); diff --git a/examples/index_pattern_field_editor_example/public/plugin.tsx b/examples/index_pattern_field_editor_example/public/plugin.tsx new file mode 100644 index 00000000000000..ccbb93e3acf956 --- /dev/null +++ b/examples/index_pattern_field_editor_example/public/plugin.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { IndexPatternFieldEditorStart } from '../../../src/plugins/index_pattern_field_editor/public'; + +interface StartDeps { + data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; +} + +interface SetupDeps { + developerExamples: DeveloperExamplesSetup; +} + +export class IndexPatternFieldEditorPlugin implements Plugin { + public setup(core: CoreSetup, deps: SetupDeps) { + core.application.register({ + id: 'indexPatternFieldEditorExample', + title: 'Index pattern field editor example', + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + const [, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + return renderApp(depsStart, params); + }, + }); + + deps.developerExamples.register({ + appId: 'indexPatternFieldEditorExample', + title: 'Index pattern field editor', + description: `IndexPatternFieldEditor provides a UI for editing index pattern fields directly from Kibana apps. This example plugin demonstrates integration.`, + links: [ + { + label: 'README', + href: + 'https://github.com/elastic/kibana/blob/master/src/plugins/index_pattern_field_editor/README.md', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/index_pattern_field_editor_example/tsconfig.json b/examples/index_pattern_field_editor_example/tsconfig.json new file mode 100644 index 00000000000000..1f6d52ed5260e4 --- /dev/null +++ b/examples/index_pattern_field_editor_example/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 6f5994a8679d68..cf6bd407d53a43 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils", "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools", - "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", + "@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath", "@kbn/ui-framework": "link:packages/kbn-ui-framework", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 083ae90a031f50..3e17d471a3cac0 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -40,6 +40,7 @@ filegroup( "//packages/kbn-securitysolution-utils:build", "//packages/kbn-securitysolution-es-utils:build", "//packages/kbn-server-http-tools:build", + "//packages/kbn-server-route-repository:build", "//packages/kbn-std:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", diff --git a/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts index 8d3fdb0f390c5e..62231f8221a957 100644 --- a/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts +++ b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts @@ -29,6 +29,16 @@ interface Manifest { server: boolean; kibanaVersion: string; version: string; + // TODO: make this required. + owner?: { + // Internally, this should be a team name. + name: string; + // All internally owned plugins should have a github team specified that can be pinged in issues, or used to look up + // members who can be asked questions regarding the plugin. + githubTeam?: string; + }; + // TODO: make required. + description?: string; serviceFolders: readonly string[]; requiredPlugins: readonly string[]; optionalPlugins: readonly string[]; @@ -66,6 +76,8 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP version: manifest.version, kibanaVersion: manifest.kibanaVersion || manifest.version, serviceFolders: manifest.serviceFolders || [], + owner: manifest.owner, + description: manifest.description, requiredPlugins: isValidDepsDeclaration(manifest.requiredPlugins, 'requiredPlugins'), optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'), requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'), diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts index 0e3d209d6398c5..55a466a430b696 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts @@ -13,20 +13,12 @@ import { REPO_ROOT, run, CiStatsReporter, createFlagError } from '@kbn/dev-utils import { Project } from 'ts-morph'; import { writePluginDocs } from './mdx/write_plugin_mdx_docs'; -import { ApiDeclaration, PluginApi, TypeKind } from './types'; +import { ApiDeclaration, ApiStats, MissingApiItemMap, PluginApi, TypeKind } from './types'; import { findPlugins } from './find_plugins'; import { pathsOutsideScopes } from './build_api_declarations/utils'; import { getPluginApiMap } from './get_plugin_api_map'; import { writeDeprecationDoc } from './mdx/write_deprecations_doc'; -export interface PluginInfo { - apiCount: number; - apiCountMissingComments: number; - id: string; - missingApiItems: string[]; - percentApiMissingComments: number; -} - function isStringArray(arr: unknown | string[]): arr is string[] { return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); } @@ -95,14 +87,13 @@ export function runBuildApiDocsCli() { const id = plugin.manifest.id; const pluginApi = pluginApiMap[id]; - const apiCount = countApiForPlugin(pluginApi); - const pluginStats = collectApiStatsForPlugin(pluginApi); + const pluginStats = collectApiStatsForPlugin(pluginApi, missingApiItems); reporter.metrics([ { id, group: 'API count', - value: apiCount, + value: pluginStats.apiCount, }, { id, @@ -202,8 +193,8 @@ export function runBuildApiDocsCli() { } } - if (apiCount > 0) { - writePluginDocs(outputFolder, pluginApi, log); + if (pluginStats.apiCount > 0) { + writePluginDocs(outputFolder, { doc: pluginApi, plugin, pluginStats, log }); } writeDeprecationDoc(outputFolder, referencedDeprecations, log); }); @@ -239,27 +230,27 @@ function getTsProject(repoPath: string) { return project; } -interface ApiStats { - missingComments: ApiDeclaration[]; - isAnyType: ApiDeclaration[]; - noReferences: ApiDeclaration[]; -} - -function collectApiStatsForPlugin(doc: PluginApi): ApiStats { - const stats: ApiStats = { missingComments: [], isAnyType: [], noReferences: [] }; +function collectApiStatsForPlugin(doc: PluginApi, missingApiItems: MissingApiItemMap): ApiStats { + const stats: ApiStats = { + missingComments: [], + isAnyType: [], + noReferences: [], + apiCount: countApiForPlugin(doc), + missingExports: Object.values(missingApiItems[doc.id] ?? {}).length, + }; Object.values(doc.client).forEach((def) => { - collectStatsForApi(def, stats); + collectStatsForApi(def, stats, doc); }); Object.values(doc.server).forEach((def) => { - collectStatsForApi(def, stats); + collectStatsForApi(def, stats, doc); }); Object.values(doc.common).forEach((def) => { - collectStatsForApi(def, stats); + collectStatsForApi(def, stats, doc); }); return stats; } -function collectStatsForApi(doc: ApiDeclaration, stats: ApiStats): void { +function collectStatsForApi(doc: ApiDeclaration, stats: ApiStats, pluginApi: PluginApi): void { const missingComment = doc.description === undefined || doc.description.length === 0; if (missingComment) { stats.missingComments.push(doc); @@ -269,7 +260,7 @@ function collectStatsForApi(doc: ApiDeclaration, stats: ApiStats): void { } if (doc.children) { doc.children.forEach((child) => { - collectStatsForApi(child, stats); + collectStatsForApi(child, stats, pluginApi); }); } if (!doc.references || doc.references.length === 0) { diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/types.ts b/packages/kbn-docs-utils/src/api_docs/mdx/types.ts new file mode 100644 index 00000000000000..38c25fe68f7bb2 --- /dev/null +++ b/packages/kbn-docs-utils/src/api_docs/mdx/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { KibanaPlatformPlugin, ToolingLog } from '@kbn/dev-utils'; +import { ApiStats, PluginApi } from '../types'; + +export interface WritePluginDocsOpts { + doc: PluginApi; + plugin: KibanaPlatformPlugin; + pluginStats: ApiStats; + log: ToolingLog; +} 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 86ddf38cba4b66..557277331b099b 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 @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/dev-utils'; import fs from 'fs'; import Path from 'path'; import dedent from 'dedent'; @@ -19,6 +18,7 @@ import { groupPluginApi, } from '../utils'; import { writePluginDocSplitByFolder } from './write_plugin_split_by_folder'; +import { WritePluginDocsOpts } from './types'; /** * Converts the plugin doc to mdx and writes it into the file system. If the plugin, @@ -28,12 +28,15 @@ import { writePluginDocSplitByFolder } from './write_plugin_split_by_folder'; * @param doc Contains the information of the plugin that will be written into mdx. * @param log Used for logging debug and error information. */ -export function writePluginDocs(folder: string, doc: PluginApi, log: ToolingLog): void { +export function writePluginDocs( + folder: string, + { doc, plugin, pluginStats, log }: WritePluginDocsOpts +): void { if (doc.serviceFolders) { log.debug(`Splitting plugin ${doc.id}`); - writePluginDocSplitByFolder(folder, doc, log); + writePluginDocSplitByFolder(folder, { doc, log, plugin, pluginStats }); } else { - writePluginDoc(folder, doc, log); + writePluginDoc(folder, { doc, plugin, pluginStats, log }); } } @@ -50,7 +53,10 @@ function hasPublicApi(doc: PluginApi): boolean { * @param doc Contains the information of the plugin that will be written into mdx. * @param log Used for logging debug and error information. */ -export function writePluginDoc(folder: string, doc: PluginApi, log: ToolingLog): void { +export function writePluginDoc( + folder: string, + { doc, log, plugin, pluginStats }: WritePluginDocsOpts +): void { if (!hasPublicApi(doc)) { log.debug(`${doc.id} does not have a public api. Skipping.`); return; @@ -62,6 +68,7 @@ export function writePluginDoc(folder: string, doc: PluginApi, log: ToolingLog): // Append "obj" to avoid special names in here. 'case' is one in particular that // caused issues. const json = getJsonName(fileName) + 'Obj'; + const name = plugin.manifest.owner?.name; let mdx = dedent(` --- @@ -74,9 +81,26 @@ date: 2020-11-16 tags: ['contributor', 'dev', 'apidocs', 'kibana', '${doc.id}'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- - import ${json} from './${fileName}.json'; +${plugin.manifest.description ?? ''} + +${ + plugin.manifest.owner?.githubTeam && name + ? `Contact [${name}](https://github.com/orgs/elastic/teams/${plugin.manifest.owner?.githubTeam}) for questions regarding this plugin.` + : name + ? `Contact ${name} for questions regarding this plugin.` + : '' +} + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| ${pluginStats.apiCount} | ${pluginStats.isAnyType.length} | ${ + pluginStats.missingComments.length + } | ${pluginStats.missingExports} | + `) + '\n\n'; const scopedDoc = { diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts index f5d547fc03520d..1a9037df0aa253 100644 --- a/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts +++ b/packages/kbn-docs-utils/src/api_docs/mdx/write_plugin_split_by_folder.ts @@ -6,17 +6,22 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/dev-utils'; import { snakeToCamel } from '../utils'; import { PluginApi, ApiDeclaration } from '../types'; import { writePluginDoc } from './write_plugin_mdx_docs'; +import { WritePluginDocsOpts } from './types'; -export function writePluginDocSplitByFolder(folder: string, doc: PluginApi, log: ToolingLog) { +export function writePluginDocSplitByFolder( + folder: string, + { doc, plugin, pluginStats, log }: WritePluginDocsOpts +) { const apisByFolder = splitApisByFolder(doc); log.debug(`Split ${doc.id} into ${apisByFolder.length} services`); apisByFolder.forEach((docDef) => { - writePluginDoc(folder, docDef, log); + // TODO: we should probably see if we can break down these stats by service folder. As it is, they will represent stats for + // the entire plugin. + writePluginDoc(folder, { doc: docDef, plugin, pluginStats, log }); }); } diff --git a/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts b/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts index ff71b0efc79d1c..c5538b9da229ea 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts +++ b/packages/kbn-docs-utils/src/api_docs/tests/api_doc_suite.test.ts @@ -13,7 +13,7 @@ import { Project } from 'ts-morph'; import { ToolingLog, KibanaPlatformPlugin } from '@kbn/dev-utils'; import { writePluginDocs } from '../mdx/write_plugin_mdx_docs'; -import { ApiDeclaration, PluginApi, Reference, TextWithLinks, TypeKind } from '../types'; +import { ApiDeclaration, ApiStats, PluginApi, Reference, TextWithLinks, TypeKind } from '../types'; import { getKibanaPlatformPlugin } from './kibana_platform_plugin_mock'; import { groupPluginApi } from '../utils'; import { getPluginApiMap } from '../get_plugin_api_map'; @@ -99,11 +99,23 @@ beforeAll(() => { const plugins: KibanaPlatformPlugin[] = [pluginA, pluginB]; const { pluginApiMap } = getPluginApiMap(project, plugins, log, { collectReferences: false }); + const pluginStats: ApiStats = { + missingComments: [], + isAnyType: [], + noReferences: [], + apiCount: 3, + missingExports: 0, + }; doc = pluginApiMap.pluginA; mdxOutputFolder = Path.resolve(__dirname, 'snapshots'); - writePluginDocs(mdxOutputFolder, doc, log); - writePluginDocs(mdxOutputFolder, pluginApiMap.pluginB, log); + writePluginDocs(mdxOutputFolder, { doc, plugin: pluginA, pluginStats, log }); + writePluginDocs(mdxOutputFolder, { + doc: pluginApiMap.pluginB, + plugin: pluginB, + pluginStats, + log, + }); }); it('Setup type is extracted', () => { diff --git a/packages/kbn-docs-utils/src/api_docs/types.ts b/packages/kbn-docs-utils/src/api_docs/types.ts index 007b8c824d3c26..de53993fe30366 100644 --- a/packages/kbn-docs-utils/src/api_docs/types.ts +++ b/packages/kbn-docs-utils/src/api_docs/types.ts @@ -231,3 +231,10 @@ export interface ApiReference { export interface ReferencedDeprecations { [key: string]: Array<{ deprecatedApi: ApiDeclaration; ref: ApiReference }>; } +export interface ApiStats { + missingComments: ApiDeclaration[]; + isAnyType: ApiDeclaration[]; + noReferences: ApiDeclaration[]; + apiCount: number; + missingExports: number; +} diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel new file mode 100644 index 00000000000000..3a146086e80bc3 --- /dev/null +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -0,0 +1,89 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-server-route-repository" +PKG_REQUIRE_NAME = "@kbn/server-route-repository" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config-schema", + "//packages/kbn-io-ts-utils", + "@npm//@hapi/boom", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//utility-types" +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json index 4ae625d83a7003..cfeab275e19cf6 100644 --- a/packages/kbn-server-route-repository/package.json +++ b/packages/kbn-server-route-repository/package.json @@ -4,10 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "private": true } diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index 8f1e72172c6759..7614a9411602ef 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-test/src/kbn_archiver_cli.ts b/packages/kbn-test/src/kbn_archiver_cli.ts index 04581a83546686..6eb1d4bf68dadc 100644 --- a/packages/kbn-test/src/kbn_archiver_cli.ts +++ b/packages/kbn-test/src/kbn_archiver_cli.ts @@ -43,13 +43,11 @@ export function runKbnArchiverCli() { new RunWithCommands({ description: 'Import/export saved objects from archives, for testing', globalFlags: { - string: ['config', 'space', 'kibana-url', 'dir'], + string: ['config', 'space', 'kibana-url'], 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 }) { @@ -79,23 +77,6 @@ export function runKbnArchiverCli() { ); } - 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'); @@ -106,7 +87,7 @@ export function runKbnArchiverCli() { kbnClient: new KbnClient({ log, url: kibanaUrl, - importExportDir, + importExportBaseDir: process.cwd(), }), }; }, diff --git a/packages/kbn-test/src/kbn_client/kbn_client.ts b/packages/kbn-test/src/kbn_client/kbn_client.ts index ac14a399918cb9..e44f5005933002 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client.ts @@ -22,7 +22,7 @@ export interface KbnClientOptions { certificateAuthorities?: Buffer[]; log: ToolingLog; uiSettingDefaults?: UiSettingValues; - importExportDir?: string; + importExportBaseDir?: string; } export class KbnClient { @@ -67,7 +67,7 @@ export class KbnClient { this.log, this.requester, this.savedObjects, - options.importExportDir + options.importExportBaseDir ); } 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 index fe67fbb70fa3ca..5fd30929fecf68 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -8,10 +8,11 @@ import { inspect } from 'util'; import Fs from 'fs/promises'; +import { existsSync } from 'fs'; import Path from 'path'; import FormData from 'form-data'; -import { ToolingLog, isAxiosResponseError, createFailError } from '@kbn/dev-utils'; +import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@kbn/dev-utils'; import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; @@ -39,7 +40,7 @@ export class KbnClientImportExport { public readonly log: ToolingLog, public readonly requester: KbnClientRequester, public readonly savedObjects: KbnClientSavedObjects, - public readonly dir?: string + public readonly baseDir: string = REPO_ROOT ) {} private resolvePath(path: string) { @@ -47,18 +48,19 @@ export class KbnClientImportExport { path = `${path}.json`; } - if (!this.dir && !Path.isAbsolute(path)) { + const absolutePath = Path.resolve(this.baseDir, path); + if (!existsSync(absolutePath)) { throw new Error( - 'unable to resolve relative path to import/export without a configured dir, either path absolute path or specify --dir' + `unable to resolve path [${path}] to import/export, resolved relative to [${this.baseDir}]` ); } - return this.dir ? Path.resolve(this.dir, path) : path; + return absolutePath; } - async load(name: string, options?: { space?: string }) { - const src = this.resolvePath(name); - this.log.debug('resolved import for', name, 'to', src); + async load(path: string, options?: { space?: string }) { + const src = this.resolvePath(path); + this.log.debug('resolved import for', path, 'to', src); const objects = await parseArchive(src); this.log.info('importing', objects.length, 'saved objects', { space: options?.space }); @@ -91,8 +93,8 @@ export class KbnClientImportExport { } } - async unload(name: string, options?: { space?: string }) { - const src = this.resolvePath(name); + async unload(path: string, options?: { space?: string }) { + const src = this.resolvePath(path); this.log.debug('unloading docs from archive at', src); const objects = await parseArchive(src); @@ -110,8 +112,8 @@ export class KbnClientImportExport { this.log.success(deleted, 'saved objects deleted'); } - async save(name: string, options: { types: string[]; space?: string }) { - const dest = this.resolvePath(name); + async save(path: string, options: { types: string[]; space?: string }) { + const dest = this.resolvePath(path); this.log.debug('saving export to', dest); const resp = await this.req(options.space, { diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 36d613ec82f9e0..d4ab8f624f7111 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -32,14 +32,14 @@ export class DocLinksService { guide: `${KIBANA_DOCS}canvas.html`, }, dashboard: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, - drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldowns`, - urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, + guide: `${KIBANA_DOCS}dashboard.html`, + drilldowns: `${KIBANA_DOCS}drilldowns.html`, + drilldownsTriggerPicker: `${KIBANA_DOCS}drilldowns.html#url-drilldowns`, + urlDrilldownTemplateSyntax: `${KIBANA_DOCS}url_templating-language.html`, + urlDrilldownVariables: `${KIBANA_DOCS}url_templating-language.html#url-template-variables`, }, discover: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/discover.html`, + guide: `${KIBANA_DOCS}discover.html`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, @@ -128,14 +128,14 @@ export class DocLinksService { luceneExpressions: `${ELASTICSEARCH_DOCS}modules-scripting-expression.html`, }, indexPatterns: { - introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, - fieldFormattersNumber: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/numeral.html`, - fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, + introduction: `${KIBANA_DOCS}index-patterns.html`, + fieldFormattersNumber: `${KIBANA_DOCS}numeral.html`, + fieldFormattersString: `${KIBANA_DOCS}field-formatters-string.html`, runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, }, - addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, - kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, - upgradeAssistant: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/upgrade-assistant.html`, + addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, + kibana: `${KIBANA_DOCS}index.html`, + upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, @@ -195,23 +195,23 @@ export class DocLinksService { }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, - kueryQuerySyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kuery-query.html`, + kueryQuerySyntax: `${KIBANA_DOCS}kuery-query.html`, luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`, percolate: `${ELASTICSEARCH_DOCS}query-dsl-percolate-query.html`, queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`, }, search: { - sessions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/search-sessions.html`, + sessions: `${KIBANA_DOCS}search-sessions.html`, }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, }, management: { - dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, + dashboardSettings: `${KIBANA_DOCS}advanced-options.html#kibana-dashboard-settings`, indexManagement: `${ELASTICSEARCH_DOCS}index-mgmt.html`, - kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, - visualizationSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-visualization-settings`, + kibanaSearchSettings: `${KIBANA_DOCS}advanced-options.html#kibana-search-settings`, + visualizationSettings: `${KIBANA_DOCS}advanced-options.html#kibana-visualization-settings`, }, ml: { guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, @@ -242,52 +242,52 @@ export class DocLinksService { guide: `${ELASTICSEARCH_DOCS}transforms.html`, }, visualize: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, - timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/timelion.html`, + guide: `${KIBANA_DOCS}dashboard.html`, + timelionDeprecation: `${KIBANA_DOCS}timelion.html`, lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, - lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, + lensPanels: `${KIBANA_DOCS}lens.html`, maps: `${ELASTIC_WEBSITE_URL}maps`, - vega: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/vega.html`, + vega: `${KIBANA_DOCS}vega.html`, }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, }, alerting: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-management.html`, - actionTypes: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/action-types.html`, - emailAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, - emailActionConfig: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, - generalSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`, - indexAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html`, - esQuery: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-es-query.html`, - indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-index-threshold.html`, - pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, - preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-connectors.html`, - preconfiguredAlertHistoryConnector: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html#preconfigured-connector-alert-history`, - serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`, - setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, - slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`, - teamsAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`, + guide: `${KIBANA_DOCS}alert-management.html`, + actionTypes: `${KIBANA_DOCS}action-types.html`, + emailAction: `${KIBANA_DOCS}email-action-type.html`, + emailActionConfig: `${KIBANA_DOCS}email-action-type.html`, + generalSettings: `${KIBANA_DOCS}alert-action-settings-kb.html#general-alert-action-settings`, + indexAction: `${KIBANA_DOCS}index-action-type.html`, + esQuery: `${KIBANA_DOCS}rule-type-es-query.html`, + indexThreshold: `${KIBANA_DOCS}rule-type-index-threshold.html`, + pagerDutyAction: `${KIBANA_DOCS}pagerduty-action-type.html`, + preconfiguredConnectors: `${KIBANA_DOCS}pre-configured-connectors.html`, + preconfiguredAlertHistoryConnector: `${KIBANA_DOCS}index-action-type.html#preconfigured-connector-alert-history`, + serviceNowAction: `${KIBANA_DOCS}servicenow-action-type.html#configuring-servicenow`, + setupPrerequisites: `${KIBANA_DOCS}alerting-getting-started.html#alerting-setup-prerequisites`, + slackAction: `${KIBANA_DOCS}slack-action-type.html#configuring-slack`, + teamsAction: `${KIBANA_DOCS}teams-action-type.html#configuring-teams`, }, maps: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, - importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, + guide: `${KIBANA_DOCS}maps.html`, + importGeospatialPrivileges: `${KIBANA_DOCS}import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { - alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, - alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`, - alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, - alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, - alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, - alertsKibanaThreadpoolRejections: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-thread-pool-rejections`, - alertsKibanaCCRReadExceptions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, - alertsKibanaLargeShardSize: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-large-shard-size`, - alertsKibanaClusterAlerts: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cluster-alerts`, + alertsKibana: `${KIBANA_DOCS}kibana-alerts.html`, + alertsKibanaCpuThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-cpu-threshold`, + alertsKibanaDiskThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-disk-usage-threshold`, + alertsKibanaJvmThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, + alertsKibanaMissingData: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-missing-monitoring-data`, + alertsKibanaThreadpoolRejections: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-thread-pool-rejections`, + alertsKibanaCCRReadExceptions: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, + alertsKibanaLargeShardSize: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-large-shard-size`, + alertsKibanaClusterAlerts: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-cluster-alerts`, metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, - monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, + monitorKibana: `${KIBANA_DOCS}monitoring-metricbeat.html`, monitorLogstash: `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html`, - troubleshootKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitor-troubleshooting.html`, + troubleshootKibana: `${KIBANA_DOCS}monitor-troubleshooting.html`, }, security: { apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, @@ -295,8 +295,8 @@ export class DocLinksService { elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, - kibanaTLS: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`, - kibanaPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-privileges.html`, + kibanaTLS: `${KIBANA_DOCS}configuring-tls.html`, + kibanaPrivileges: `${KIBANA_DOCS}kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, runAsPrivilege: `${ELASTICSEARCH_DOCS}security-privileges.html#_run_as_privilege`, @@ -305,7 +305,7 @@ export class DocLinksService { jiraAction: `${ELASTICSEARCH_DOCS}actions-jira.html`, pagerDutyAction: `${ELASTICSEARCH_DOCS}actions-pagerduty.html`, slackAction: `${ELASTICSEARCH_DOCS}actions-slack.html`, - ui: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/watcher-ui.html`, + ui: `${KIBANA_DOCS}watcher-ui.html`, }, ccs: { guide: `${ELASTICSEARCH_DOCS}modules-cross-cluster-search.html`, diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index 7ac629534ba089..b59418a67219e0 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -48,6 +48,8 @@ const KNOWN_MANIFEST_FIELDS = (() => { extraPublicDirs: true, requiredBundles: true, serviceFolders: true, + owner: true, + description: true, }; return new Set(Object.keys(manifestFields)); @@ -187,6 +189,8 @@ export async function parseManifest( ui: includesUiPlugin, server: includesServerPlugin, extraPublicDirs: manifest.extraPublicDirs, + owner: manifest.owner, + description: manifest.description, }; } diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 6b50b5e820665e..0cdc806e997ef7 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -208,6 +208,27 @@ export interface PluginManifest { * folders will cause your plugin API reference to be broken up into sub sections. */ readonly serviceFolders?: readonly string[]; + + /** + * TODO: make required once all internal plugins have this specified. + */ + readonly owner?: { + /** + * The name of the team that currently owns this plugin. + */ + readonly name: string; + /** + * All internal plugins should have a github team specified. GitHub teams can be viewed here: + * https://github.com/orgs/elastic/teams + */ + readonly githubTeam?: string; + }; + + /** + * TODO: make required once all plugins specify this. + * A brief description of what this plugin does and any capabilities it provides. + */ + readonly description?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index acec03902bf6aa..ce13174ee19cc2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1917,11 +1917,16 @@ export interface PluginInitializerContext { export interface PluginManifest { // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported readonly configPath: ConfigPath; + readonly description?: string; // @deprecated readonly extraPublicDirs?: string[]; readonly id: PluginName; readonly kibanaVersion: string; readonly optionalPlugins: readonly PluginName[]; + readonly owner?: { + readonly name: string; + readonly githubTeam?: string; + }; readonly requiredBundles: readonly string[]; readonly requiredPlugins: readonly PluginName[]; readonly server: boolean; @@ -3260,9 +3265,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/elasticsearch/client/types.ts:94:7 - (ae-forgotten-export) The symbol "Explanation" needs to be exported by the entry point index.d.ts // src/core/server/http/router/response.ts:301:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:326:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:326:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:329:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:434:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" +// src/core/server/plugins/types.ts:347:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:347:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:350:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:455:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "create" ``` diff --git a/src/plugins/bfetch/kibana.json b/src/plugins/bfetch/kibana.json index 9f9f2176af671f..6d37d68ffd584f 100644 --- a/src/plugins/bfetch/kibana.json +++ b/src/plugins/bfetch/kibana.json @@ -3,5 +3,10 @@ "version": "kibana", "server": true, "ui": true, - "requiredBundles": ["kibanaUtils"] + "requiredBundles": ["kibanaUtils"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back." } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index c897cdbce2309a..0c3a9901f8c8ca 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -364,7 +364,6 @@ export class IndexPattern implements IIndexPattern { * @param name Field name * @param runtimeField Runtime field definition */ - addRuntimeField(name: string, runtimeField: RuntimeField) { const existingField = this.getFieldByName(name); if (existingField) { @@ -384,11 +383,41 @@ export class IndexPattern implements IIndexPattern { } /** - * Remove a runtime field - removed from mapped field or removed unmapped - * field as appropriate - * @param name Field name + * Checks if runtime field exists + * @param name + */ + hasRuntimeField(name: string): boolean { + return !!this.runtimeFieldMap[name]; + } + + /** + * Returns runtime field if exists + * @param name + */ + getRuntimeField(name: string): RuntimeField | null { + return this.runtimeFieldMap[name] ?? null; + } + + /** + * Replaces all existing runtime fields with new fields + * @param newFields */ + replaceAllRuntimeFields(newFields: Record) { + const oldRuntimeFieldNames = Object.keys(this.runtimeFieldMap); + oldRuntimeFieldNames.forEach((name) => { + this.removeRuntimeField(name); + }); + Object.entries(newFields).forEach(([name, field]) => { + this.addRuntimeField(name, field); + }); + } + + /** + * Remove a runtime field - removed from mapped field or removed unmapped + * field as appropriate. Doesn't clear associated field attributes. + * @param name - Field name to remove + */ removeRuntimeField(name: string) { const existingField = this.getFieldByName(name); if (existingField) { @@ -396,9 +425,6 @@ export class IndexPattern implements IIndexPattern { // mapped field, remove runtimeField def existingField.runtimeField = undefined; } else { - // runtimeField only - this.setFieldCustomLabel(name, null); - this.deleteFieldFormat(name); this.fields.remove(existingField); } } diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 4e9e4c318c9575..e425d0701155bd 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -3,19 +3,14 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": [ - "bfetch", - "expressions", - "uiActions", - "share", - "inspector" - ], + "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector"], "serviceFolders": ["search", "index_patterns", "query", "autocomplete", "ui", "field_formats"], "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], - "requiredBundles": [ - "kibanaUtils", - "kibanaReact", - "inspector" - ] + "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters." } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5ef499840fa6de..67534577d99fcf 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1413,6 +1413,7 @@ export class IndexPattern implements IIndexPattern { typeMeta?: string | undefined; type?: string | undefined; }; + getRuntimeField(name: string): RuntimeField | null; // @deprecated (undocumented) getScriptedFields(): IndexPatternField[]; getSourceFiltering(): { @@ -1420,6 +1421,7 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getTimeField(): IndexPatternField | undefined; + hasRuntimeField(name: string): boolean; // (undocumented) id?: string; // @deprecated (undocumented) @@ -1433,6 +1435,7 @@ export class IndexPattern implements IIndexPattern { removeRuntimeField(name: string): void; // @deprecated removeScriptedField(fieldName: string): void; + replaceAllRuntimeFields(newFields: Record): void; resetOriginalSavedObjectBody: () => void; // (undocumented) protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 9bff590b54f1c1..d2d8cb82cf646e 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -21,6 +21,11 @@ import { registerDeleteScriptedFieldRoute } from './routes/scripted_fields/delet import { registerUpdateScriptedFieldRoute } from './routes/scripted_fields/update_scripted_field'; import type { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { registerManageDefaultIndexPatternRoutes } from './routes/default_index_pattern'; +import { registerCreateRuntimeFieldRoute } from './routes/runtime_fields/create_runtime_field'; +import { registerGetRuntimeFieldRoute } from './routes/runtime_fields/get_runtime_field'; +import { registerDeleteRuntimeFieldRoute } from './routes/runtime_fields/delete_runtime_field'; +import { registerPutRuntimeFieldRoute } from './routes/runtime_fields/put_runtime_field'; +import { registerUpdateRuntimeFieldRoute } from './routes/runtime_fields/update_runtime_field'; export function registerRoutes( http: HttpServiceSetup, @@ -55,6 +60,13 @@ export function registerRoutes( registerDeleteScriptedFieldRoute(router, getStartServices); registerUpdateScriptedFieldRoute(router, getStartServices); + // Runtime Fields API + registerCreateRuntimeFieldRoute(router, getStartServices); + registerGetRuntimeFieldRoute(router, getStartServices); + registerDeleteRuntimeFieldRoute(router, getStartServices); + registerPutRuntimeFieldRoute(router, getStartServices); + registerUpdateRuntimeFieldRoute(router, getStartServices); + router.get( { path: '/api/index_patterns/_fields_for_wildcard', diff --git a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts index d0767334626229..7049903f84e8c2 100644 --- a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts +++ b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts @@ -9,7 +9,11 @@ import { schema } from '@kbn/config-schema'; import { IndexPatternSpec } from 'src/plugins/data/common'; import { handleErrors } from './util/handle_errors'; -import { fieldSpecSchema, serializedFieldFormatSchema } from './util/schemas'; +import { + fieldSpecSchema, + runtimeFieldSpecSchema, + serializedFieldFormatSchema, +} from './util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; @@ -39,6 +43,7 @@ const indexPatternSpecSchema = schema.object({ ) ), allowNoIndex: schema.maybe(schema.boolean()), + runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSpecSchema)), }); export const registerCreateIndexPatternRoute = ( @@ -66,6 +71,7 @@ export const registerCreateIndexPatternRoute = ( elasticsearchClient ); const body = req.body; + const indexPattern = await indexPatternsService.createAndSave( body.index_pattern as IndexPatternSpec, body.override, diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts new file mode 100644 index 00000000000000..faf6d87b6d10bf --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { runtimeFieldSpecSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerCreateRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + body: schema.object({ + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + runtimeField: runtimeFieldSpecSchema, + }), + }, + }, + + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { name, runtimeField } = req.body; + + const indexPattern = await indexPatternsService.get(id); + + if (indexPattern.fields.getByName(name)) { + throw new Error(`Field [name = ${name}] already exists.`); + } + + indexPattern.addRuntimeField(name, runtimeField); + + const addedField = indexPattern.fields.getByName(name); + if (!addedField) throw new Error(`Could not create a field [name = ${name}].`); + + await indexPatternsService.updateSavedObject(indexPattern); + + const savedField = indexPattern.fields.getByName(name); + if (!savedField) throw new Error(`Could not create a field [name = ${name}].`); + + return res.ok({ + body: { + field: savedField.toSpec(), + index_pattern: indexPattern.toSpec(), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts new file mode 100644 index 00000000000000..58b8529d7cf5af --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts @@ -0,0 +1,64 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerDeleteRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.delete( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field/{name}', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + + const indexPattern = await indexPatternsService.get(id); + const field = indexPattern.fields.getByName(name); + + if (!field) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!field.runtimeField) { + throw new Error('Only runtime fields can be deleted.'); + } + + indexPattern.removeRuntimeField(name); + + await indexPatternsService.updateSavedObject(indexPattern); + + return res.ok(); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts new file mode 100644 index 00000000000000..6bc2bf396c0b44 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { schema } from '@kbn/config-schema'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerGetRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.get( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field/{name}', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + }, + }, + + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + + const indexPattern = await indexPatternsService.get(id); + + const field = indexPattern.fields.getByName(name); + + if (!field) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!field.runtimeField) { + throw new Error('Only runtime fields can be retrieved.'); + } + + return res.ok({ + body: { + field: field.toSpec(), + runtimeField: indexPattern.getRuntimeField(name), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts new file mode 100644 index 00000000000000..a5e92fa5a36ecb --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts @@ -0,0 +1,76 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { runtimeFieldSpecSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerPutRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.put( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + body: schema.object({ + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + runtimeField: runtimeFieldSpecSchema, + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { name, runtimeField } = req.body; + + const indexPattern = await indexPatternsService.get(id); + + const oldFieldObject = indexPattern.fields.getByName(name); + + if (oldFieldObject && !oldFieldObject.runtimeField) { + throw new Error('Only runtime fields can be updated'); + } + + if (oldFieldObject) { + indexPattern.removeRuntimeField(name); + } + + indexPattern.addRuntimeField(name, runtimeField); + + await indexPatternsService.updateSavedObject(indexPattern); + + const fieldObject = indexPattern.fields.getByName(name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${name}].`); + + return res.ok({ + body: { + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts new file mode 100644 index 00000000000000..3f3aae46c43882 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.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 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 { schema } from '@kbn/config-schema'; +import { RuntimeField } from 'src/plugins/data/common'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { runtimeFieldSpec, runtimeFieldSpecTypeSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerUpdateRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field/{name}', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + body: schema.object({ + name: schema.never(), + runtimeField: schema.object({ + ...runtimeFieldSpec, + // We need to overwrite the below fields on top of `runtimeFieldSpec`, + // because some fields would be optional + type: schema.maybe(runtimeFieldSpecTypeSchema), + }), + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + const runtimeField = req.body.runtimeField as Partial; + + const indexPattern = await indexPatternsService.get(id); + const existingRuntimeField = indexPattern.getRuntimeField(name); + + if (!existingRuntimeField) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + indexPattern.removeRuntimeField(name); + indexPattern.addRuntimeField(name, { + ...existingRuntimeField, + ...runtimeField, + }); + + await indexPatternsService.updateSavedObject(indexPattern); + + const fieldObject = indexPattern.fields.getByName(name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${name}].`); + + return res.ok({ + body: { + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts index c1509b9b848bef..1c88550c154c56 100644 --- a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts +++ b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts @@ -8,7 +8,11 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from './util/handle_errors'; -import { fieldSpecSchema, serializedFieldFormatSchema } from './util/schemas'; +import { + fieldSpecSchema, + runtimeFieldSpecSchema, + serializedFieldFormatSchema, +} from './util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; @@ -28,6 +32,7 @@ const indexPatternUpdateSchema = schema.object({ fieldFormats: schema.maybe(schema.recordOf(schema.string(), serializedFieldFormatSchema)), fields: schema.maybe(schema.recordOf(schema.string(), fieldSpecSchema)), allowNoIndex: schema.maybe(schema.boolean()), + runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSpecSchema)), }); export const registerUpdateIndexPatternRoute = ( @@ -78,6 +83,7 @@ export const registerUpdateIndexPatternRoute = ( type, typeMeta, fields, + runtimeFieldMap, }, } = req.body; @@ -131,6 +137,11 @@ export const registerUpdateIndexPatternRoute = ( ); } + if (runtimeFieldMap !== undefined) { + changeCount++; + indexPattern.replaceAllRuntimeFields(runtimeFieldMap); + } + if (changeCount < 1) { throw new Error('Index pattern change set is empty.'); } diff --git a/src/plugins/data/server/index_patterns/routes/util/schemas.ts b/src/plugins/data/server/index_patterns/routes/util/schemas.ts index d916423c4fc69c..79ee1ffa1ab970 100644 --- a/src/plugins/data/server/index_patterns/routes/util/schemas.ts +++ b/src/plugins/data/server/index_patterns/routes/util/schemas.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; +import { RUNTIME_FIELD_TYPES, RuntimeType } from '../../../../common'; export const serializedFieldFormatSchema = schema.object({ id: schema.maybe(schema.string()), @@ -52,4 +53,24 @@ export const fieldSpecSchemaFields = { shortDotsEnable: schema.maybe(schema.boolean()), }; -export const fieldSpecSchema = schema.object(fieldSpecSchemaFields); +export const fieldSpecSchema = schema.object(fieldSpecSchemaFields, { + // Allow and ignore unknowns to make fields transient. + // Because `fields` have a bunch of calculated fields + // this allows to retrieve an index pattern and then to re-create by using the retrieved payload + unknowns: 'ignore', +}); + +export const runtimeFieldSpecTypeSchema = schema.oneOf( + RUNTIME_FIELD_TYPES.map((runtimeFieldType) => schema.literal(runtimeFieldType)) as [ + Type + ] +); +export const runtimeFieldSpec = { + type: runtimeFieldSpecTypeSchema, + script: schema.maybe( + schema.object({ + source: schema.string(), + }) + ), +}; +export const runtimeFieldSpecSchema = schema.object(runtimeFieldSpec); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ff265ccf533015..783bd8d2fcd0e1 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -816,6 +816,7 @@ export class IndexPattern implements IIndexPattern { typeMeta?: string | undefined; type?: string | undefined; }; + getRuntimeField(name: string): RuntimeField | null; // @deprecated (undocumented) getScriptedFields(): IndexPatternField[]; getSourceFiltering(): { @@ -823,6 +824,7 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getTimeField(): IndexPatternField | undefined; + hasRuntimeField(name: string): boolean; // (undocumented) id?: string; // @deprecated (undocumented) @@ -836,6 +838,7 @@ export class IndexPattern implements IIndexPattern { removeRuntimeField(name: string): void; // @deprecated removeScriptedField(fieldName: string): void; + replaceAllRuntimeFields(newFields: Record): void; resetOriginalSavedObjectBody: () => void; // (undocumented) protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index ce987e2870466b..0430614d413b6b 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -346,6 +346,7 @@ export function DiscoverLayout({ verticalPosition={contentCentered ? 'center' : undefined} horizontalPosition={contentCentered ? 'center' : undefined} paddingSize="none" + hasShadow={false} className={classNames('dscPageContent', { 'dscPageContent--centered': contentCentered, })} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index 63d16af7de478a..60841799b1398b 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -143,4 +143,24 @@ describe('Discover flyout', function () { expect(props.setExpandedDoc).toHaveBeenCalledTimes(1); expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4'); }); + + it('allows navigating with arrow keys through documents', () => { + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); + expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ _id: '2' })); + component.setProps({ ...props, hit: props.hits[1] }); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' }); + expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ _id: '1' })); + }); + + it('should not navigate with keypresses when already at the border of documents', () => { + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' }); + expect(props.setExpandedDoc).not.toHaveBeenCalled(); + component.setProps({ ...props, hit: props.hits[props.hits.length - 1] }); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); + expect(props.setExpandedDoc).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index 3894127891041c..aaae9afe6531a5 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -21,6 +21,7 @@ import { EuiPortal, EuiPagination, EuiHideFor, + keys, } from '@elastic/eui'; import { DocViewer } from '../doc_viewer/doc_viewer'; import { IndexPattern } from '../../../kibana_services'; @@ -87,9 +88,25 @@ export function DiscoverGridFlyout({ [hits, setExpandedDoc] ); + const onKeyDown = useCallback( + (ev: React.KeyboardEvent) => { + if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) { + ev.preventDefault(); + ev.stopPropagation(); + setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1)); + } + }, + [activePage, setPage] + ); + return ( - + { ] `); }); + test('returns the same instance of ["_source"] over multiple calls', async () => { + const result = getDisplayedColumns([], indexPatternWithTimefieldMock); + const result2 = getDisplayedColumns([], indexPatternWithTimefieldMock); + expect(result).toBe(result2); + }); }); diff --git a/src/plugins/discover/public/application/helpers/columns.ts b/src/plugins/discover/public/application/helpers/columns.ts index 426059060f329b..6e77717c5cf054 100644 --- a/src/plugins/discover/public/application/helpers/columns.ts +++ b/src/plugins/discover/public/application/helpers/columns.ts @@ -8,6 +8,11 @@ import { IndexPattern } from '../../../../data/common'; +// We store this outside the function as a constant, so we're not creating a new array every time +// the function is returning this. A changing array might cause the data grid to think it got +// new columns, and thus performing worse than using the same array over multiple renders. +const SOURCE_ONLY = ['_source']; + /** * Function to provide fallback when * 1) no columns are given @@ -19,5 +24,5 @@ export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: I // check if all columns where removed except the configured timeField (this can't be removed) !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName) ? stateColumns - : ['_source']; + : SOURCE_ONLY; } diff --git a/src/plugins/embeddable/common/lib/migrate.ts b/src/plugins/embeddable/common/lib/migrate.ts index 0a9cd0760eb2ce..fb8ea5cf2cd84e 100644 --- a/src/plugins/embeddable/common/lib/migrate.ts +++ b/src/plugins/embeddable/common/lib/migrate.ts @@ -26,9 +26,11 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) = updatedInput.enhancements = {}; Object.keys(enhancements).forEach((key) => { if (!enhancements[key]) return; - (updatedInput.enhancements! as Record)[key] = embeddables - .getEnhancement(key) - .migrations[version](enhancements[key] as SerializableState); + const enhancementDefinition = embeddables.getEnhancement(key); + const migratedEnhancement = enhancementDefinition?.migrations?.[version] + ? enhancementDefinition.migrations[version](enhancements[key] as SerializableState) + : enhancements[key]; + (updatedInput.enhancements! as Record)[key] = migratedEnhancement; }); return updatedInput; diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts index 2e7d8c73cfc6da..53302e8e6870ce 100644 --- a/src/plugins/embeddable/public/plugin.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -184,4 +184,12 @@ describe('embeddable enhancements', () => { embeddableState.enhancements.test ); }); + + test('doesnt fail if there is no migration function registered for specific version', () => { + expect(() => { + start.migrate(embeddableState, '7.10.0'); + }).not.toThrow(); + + expect(start.migrate(embeddableState, '7.10.0')).toEqual(embeddableState); + }); }); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap index 645694371f9059..1310488c65fab8 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -59,7 +59,6 @@ exports[`EmptyState should render normally 1`] = ` } - isDisabled={false} onClick={[Function]} title={ { docLinks={docLinks} onRefresh={() => {}} navigateToApp={async () => {}} - getMlCardState={() => MlCardState.ENABLED} canSave={true} /> ); @@ -48,7 +46,6 @@ describe('EmptyState', () => { docLinks={docLinks} onRefresh={onRefreshHandler} navigateToApp={async () => {}} - getMlCardState={() => MlCardState.ENABLED} canSave={true} /> ); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx index 438eb8a031993f..240e732752916c 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -8,7 +8,6 @@ import './empty_state.scss'; import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocLinksStart, ApplicationStart } from 'kibana/public'; import { @@ -28,60 +27,18 @@ import { } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public'; -import { MlCardState } from '../../../types'; export const EmptyState = ({ onRefresh, navigateToApp, docLinks, - getMlCardState, canSave, }: { onRefresh: () => void; navigateToApp: ApplicationStart['navigateToApp']; docLinks: DocLinksStart; - getMlCardState: () => MlCardState; canSave: boolean; }) => { - const mlCard = ( - - navigateToApp('ml', { path: '#/filedatavisualizer' })} - className="inpEmptyState__card" - betaBadgeLabel={ - getMlCardState() === MlCardState.ENABLED - ? undefined - : i18n.translate( - 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel', - { - defaultMessage: 'Basic', - } - ) - } - betaBadgeTooltipContent={i18n.translate( - 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription', - { - defaultMessage: 'This feature requires a Basic license.', - } - )} - isDisabled={getMlCardState() === MlCardState.DISABLED} - icon={} - title={ - - } - description={ - - } - /> - - ); - const createAnyway = ( - {getMlCardState() !== MlCardState.HIDDEN ? mlCard : <>} + + navigateToApp('home', { path: '#/tutorial_directory/fileDataViz' })} + className="inpEmptyState__card" + icon={} + title={ + + } + description={ + + } + /> + { application, http, data, - getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); const [creationOptions, setCreationOptions] = useState([]); @@ -182,7 +181,6 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { onRefresh={loadSources} docLinks={docLinks} navigateToApp={application.navigateToApp} - getMlCardState={getMlCardState} canSave={canSave} /> ); diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 94611705a93908..726c055d1b8c34 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -30,5 +30,3 @@ export { IndexPatternCreationOption, IndexPatternListConfig, } from './service'; - -export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 355f529fe0f759..ec5b7c74020a5a 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -23,7 +23,7 @@ import { CreateIndexPatternWizardWithRouter, } from '../components'; import { IndexPatternManagementStartDependencies, IndexPatternManagementStart } from '../plugin'; -import { IndexPatternManagmentContext, MlCardState } from '../types'; +import { IndexPatternManagmentContext } from '../types'; const readOnlyBadge = { text: i18n.translate('indexPatternManagement.indexPatterns.badge.readOnly.text', { @@ -37,8 +37,7 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, - params: ManagementAppMountParams, - getMlCardState: () => MlCardState + params: ManagementAppMountParams ) { const [ { chrome, application, uiSettings, notifications, overlays, http, docLinks }, @@ -63,7 +62,6 @@ export async function mountManagementSection( indexPatternFieldEditor, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, - getMlCardState, fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 3462131e50463b..6c709fb14f08d7 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -26,9 +26,6 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ list: { addListConfig: jest.fn(), } as any, - environment: { - update: jest.fn(), - }, }); const createStartContract = (): IndexPatternManagementStart => ({ @@ -93,7 +90,6 @@ const createIndexPatternManagmentContext = (): { indexPatternFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, - getMlCardState: () => 2, fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; }; diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ed92172c8b91ca..e3c156927bface 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -77,9 +77,7 @@ export class IndexPatternManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params, () => - this.indexPatternManagementService.environmentService.getEnvironment().ml() - ); + return mountManagementSection(core.getStartServices, params); }, }); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts deleted file mode 100644 index 1eaab2eaccc111..00000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 type { PublicMethodsOf } from '@kbn/utility-types'; -import { EnvironmentService, EnvironmentServiceSetup } from './environment'; -import { MlCardState } from '../../types'; - -const createSetupMock = (): jest.Mocked => { - const setup = { - update: jest.fn(), - }; - return setup; -}; - -const createMock = (): jest.Mocked> => { - const service = { - setup: jest.fn(), - getEnvironment: jest.fn(() => ({ - ml: () => MlCardState.ENABLED, - })), - }; - service.setup.mockImplementation(createSetupMock); - return service; -}; - -export const environmentServiceMock = { - createSetup: createSetupMock, - create: createMock, -}; diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts deleted file mode 100644 index 9e571374b4784f..00000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts +++ /dev/null @@ -1,38 +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 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 { EnvironmentService } from './environment'; -import { MlCardState } from '../../types'; - -describe('EnvironmentService', () => { - describe('setup', () => { - test('allows multiple update calls', () => { - const setup = new EnvironmentService().setup(); - expect(() => { - setup.update({ ml: () => MlCardState.ENABLED }); - }).not.toThrow(); - }); - }); - - describe('getEnvironment', () => { - test('returns default values', () => { - const service = new EnvironmentService(); - expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); - }); - - test('returns last state of update calls', () => { - let cardState = MlCardState.DISABLED; - const service = new EnvironmentService(); - const setup = service.setup(); - setup.update({ ml: () => cardState }); - expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); - cardState = MlCardState.ENABLED; - expect(service.getEnvironment().ml()).toEqual(MlCardState.ENABLED); - }); - }); -}); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.ts b/src/plugins/index_pattern_management/public/service/environment/environment.ts deleted file mode 100644 index 7bf0c1eb52068a..00000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { MlCardState } from '../../types'; - -/** @public */ -export interface Environment { - /** - * Flag whether ml features should be advertised - */ - readonly ml: () => MlCardState; -} - -export class EnvironmentService { - private environment = { - ml: () => MlCardState.DISABLED, - }; - - public setup() { - return { - /** - * Update the environment to influence how available features are presented. - * @param update - */ - update: (update: Partial) => { - this.environment = Object.assign({}, this.environment, update); - }, - }; - } - - public getEnvironment() { - return this.environment; - } -} - -export type EnvironmentServiceSetup = ReturnType; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index 15be7f11892e49..f30ccfcb9f3ed7 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -9,7 +9,6 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; -import { EnvironmentService } from './environment'; interface SetupDependencies { httpClient: HttpSetup; } @@ -22,12 +21,10 @@ interface SetupDependencies { export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; - environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); - this.environmentService = new EnvironmentService(); } public setup({ httpClient }: SetupDependencies) { @@ -40,7 +37,6 @@ export class IndexPatternManagementService { return { creation: creationManagerSetup, list: indexPatternListConfigSetup, - environment: this.environmentService.setup(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 58a138df633fd3..a61eeb99b25a57 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -33,14 +33,7 @@ export interface IndexPatternManagmentContext { indexPatternFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; - getMlCardState: () => MlCardState; fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; } export type IndexPatternManagmentContextValue = KibanaReactContextValue; - -export enum MlCardState { - HIDDEN, - DISABLED, - ENABLED, -} diff --git a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts index 3bec22de48ee66..16861f3c28051e 100644 --- a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts +++ b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts @@ -22,6 +22,7 @@ export default function ({ getService }: FtrProviderContext) { indexPattern = ( await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title: basicIndex, }, diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js index 9c1e1bba0ab9a5..656b4e506fa23d 100644 --- a/test/api_integration/apis/index_patterns/index.js +++ b/test/api_integration/apis/index_patterns/index.js @@ -15,5 +15,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./scripted_fields_crud')); loadTestFile(require.resolve('./fields_api')); loadTestFile(require.resolve('./default_index_pattern')); + loadTestFile(require.resolve('./runtime_fields_crud')); + loadTestFile(require.resolve('./integration')); }); } diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts index 31c3f13a6e05fb..500a642f60850e 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts @@ -117,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body.index_pattern.fields.bar.type).to.be('boolean'); }); - it('Can add scripted fields, other fields created from es index', async () => { + it('can add scripted fields, other fields created from es index', async () => { const title = `basic_index*`; const response = await supertest.post('/api/index_patterns/index_pattern').send({ override: true, @@ -159,6 +159,32 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body.index_pattern.fields.bar.esTypes[0]).to.be('test-type'); expect(response.body.index_pattern.fields.bar.scripted).to.be(true); }); + + it('can add runtime fields', async () => { + const title = `basic_index*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + + expect(response.body.index_pattern.runtimeFieldMap.runtimeFoo.type).to.be('keyword'); + expect(response.body.index_pattern.runtimeFieldMap.runtimeFoo.script.source).to.be( + 'emit(doc["foo"].value)' + ); + }); }); it('can specify optional typeMeta attribute when creating an index pattern', async () => { diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts index 2f62ea231b7229..598001644eedbe 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts @@ -64,5 +64,25 @@ export default function ({ getService }: FtrProviderContext) { '[request body.refresh_fields]: expected value of type [boolean] but got [number]' ); }); + + it('returns an error when unknown runtime field type', async () => { + const title = `basic_index*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'wrong-type', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response.status).to.be(400); + }); }); } diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts index cd34724e6cda35..7532278d7eb128 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts @@ -284,5 +284,53 @@ export default function ({ getService }: FtrProviderContext) { expect(response3.body.index_pattern.intervalName).to.be('intervalName2'); expect(response3.body.index_pattern.typeMeta.baz).to.be('qux'); }); + + it('can update runtime fields', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.title).to.be(title); + + expect(response1.body.index_pattern.runtimeFieldMap.runtimeFoo.type).to.be('keyword'); + expect(response1.body.index_pattern.runtimeFieldMap.runtimeFoo.script.source).to.be( + 'emit(doc["foo"].value)' + ); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + runtimeFieldMap: { + runtimeBar: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response2.body.index_pattern.runtimeFieldMap.runtimeBar.type).to.be('keyword'); + expect(response2.body.index_pattern.runtimeFieldMap.runtimeFoo).to.be(undefined); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.runtimeFieldMap.runtimeBar.type).to.be('keyword'); + expect(response3.body.index_pattern.runtimeFieldMap.runtimeFoo).to.be(undefined); + }); }); } diff --git a/test/api_integration/apis/index_patterns/integration/index.ts b/test/api_integration/apis/index_patterns/integration/index.ts new file mode 100644 index 00000000000000..6fd5f644ae8949 --- /dev/null +++ b/test/api_integration/apis/index_patterns/integration/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +/** + * Test usage of different index patterns APIs in combination + */ +export default function ({ loadTestFile }: FtrProviderContext) { + describe('integration', () => { + loadTestFile(require.resolve('./integration')); + }); +} diff --git a/test/api_integration/apis/index_patterns/integration/integration.ts b/test/api_integration/apis/index_patterns/integration/integration.ts new file mode 100644 index 00000000000000..22f07553733235 --- /dev/null +++ b/test/api_integration/apis/index_patterns/integration/integration.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import _ from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +/** + * Test usage of different index patterns APIs in combination + */ +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('integration', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('create an index pattern, add a runtime field, add a field formatter, then re-create the same index pattern', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + runtimeBar: { + count: 123, + customLabel: 'test', + }, + }, + }); + + expect(response3.status).to.be(200); + + const response4 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + runtimeBar: { + format: { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'humanizePrecise' }, + }, + }, + }, + }); + + expect(response4.status).to.be(200); + + const response5 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response5.status).to.be(200); + + const resultIndexPattern = response5.body.index_pattern; + + const runtimeField = resultIndexPattern.fields.runtimeBar; + expect(runtimeField.name).to.be('runtimeBar'); + expect(runtimeField.runtimeField.type).to.be('long'); + expect(runtimeField.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(runtimeField.scripted).to.be(false); + + expect(resultIndexPattern.fieldFormats.runtimeBar.id).to.be('duration'); + expect(resultIndexPattern.fieldFormats.runtimeBar.params.inputFormat).to.be('milliseconds'); + expect(resultIndexPattern.fieldFormats.runtimeBar.params.outputFormat).to.be( + 'humanizePrecise' + ); + + expect(resultIndexPattern.fieldAttrs.runtimeBar.count).to.be(123); + expect(resultIndexPattern.fieldAttrs.runtimeBar.customLabel).to.be('test'); + + // check that retrieved object is transient and a clone can be created + const response6 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: resultIndexPattern, + }); + + expect(response6.status).to.be(200); + const recreatedIndexPattern = response6.body.index_pattern; + + expect(_.omit(recreatedIndexPattern, 'version')).to.eql( + _.omit(resultIndexPattern, 'version') + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/errors.ts new file mode 100644 index 00000000000000..8ce9e3b36b5c8d --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/errors.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns an error field object is not provided', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({}); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be( + '[request body.name]: expected value of type [string] but got [undefined]' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/index.ts new file mode 100644 index 00000000000000..2cb90ca087f497 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('create_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts new file mode 100644 index 00000000000000..e262b9d838e97a --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts @@ -0,0 +1,95 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can create a new runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.field.name).to.be('runtimeBar'); + expect(response2.body.field.runtimeField.type).to.be('long'); + expect(response2.body.field.runtimeField.script.source).to.be( + "emit(doc['field_name'].value)" + ); + expect(response2.body.field.scripted).to.be(false); + }); + + it('newly created runtime field is available in the index_pattern object', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + + await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response2.status).to.be(200); + + const field = response2.body.index_pattern.fields.runtimeBar; + + expect(field.name).to.be('runtimeBar'); + expect(field.runtimeField.type).to.be('long'); + expect(field.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(field.scripted).to.be(false); + await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts new file mode 100644 index 00000000000000..b41a630889ff8a --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts @@ -0,0 +1,81 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('errors', () => { + const basicIndex = 'b*sic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.delete( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing runtime field', async () => { + const response1 = await supertest.delete( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/test` + ); + + expect(response1.status).to.be(404); + }); + + it('returns error when attempting to delete a field which is not a runtime field', async () => { + const response2 = await supertest.delete( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be deleted.'); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.delete( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/index.ts new file mode 100644 index 00000000000000..a14201e750ddaf --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('delete_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/main.ts new file mode 100644 index 00000000000000..3c74aa336e440c --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/main.ts @@ -0,0 +1,66 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can delete a runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeBar: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(typeof response2.body.index_pattern.fields.runtimeBar).to.be('object'); + + const response3 = await supertest.delete( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field/runtimeBar` + ); + + expect(response3.status).to.be(200); + + const response4 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(typeof response4.body.index_pattern.fields.runtimeBar).to.be('undefined'); + await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts new file mode 100644 index 00000000000000..3608089e4641a2 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts @@ -0,0 +1,81 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('errors', () => { + const basicIndex = '*asic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.get( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing runtime field', async () => { + const response1 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/sf` + ); + + expect(response1.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.get( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + + it('returns error when attempting to fetch a field which is not a runtime field', async () => { + const response2 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be retrieved.'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/index.ts new file mode 100644 index 00000000000000..2e48ba64841eeb --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/main.ts new file mode 100644 index 00000000000000..fa0283d69d8e3b --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/main.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can fetch a runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeFoo' + ); + + expect(response2.status).to.be(200); + expect(typeof response2.body.field).to.be('object'); + expect(response2.body.field.name).to.be('runtimeFoo'); + expect(response2.body.field.type).to.be('string'); + expect(response2.body.field.scripted).to.be(false); + expect(response2.body.field.runtimeField.script.source).to.be( + "emit(doc['field_name'].value)" + ); + await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/index.ts new file mode 100644 index 00000000000000..7a727a3e867550 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('runtime_fields_crud', () => { + loadTestFile(require.resolve('./create_runtime_field')); + loadTestFile(require.resolve('./get_runtime_field')); + loadTestFile(require.resolve('./delete_runtime_field')); + loadTestFile(require.resolve('./put_runtime_field')); + loadTestFile(require.resolve('./update_runtime_field')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts new file mode 100644 index 00000000000000..9faca08238033e --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('errors', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .put(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error on non-runtime field update attempt', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + + const response2 = await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'bar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be updated'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/index.ts new file mode 100644 index 00000000000000..724f18e57f3efd --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('put_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/main.ts new file mode 100644 index 00000000000000..92d8c6fd6d3c25 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/main.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can overwrite an existing field', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + const response2 = await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'runtimeFoo', + runtimeField: { + type: 'long', + script: { + source: "doc['field_name'].value", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeFoo' + ); + + expect(response3.status).to.be(200); + expect(response3.body.field.type).to.be('number'); + + const response4 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeBar' + ); + + expect(response4.status).to.be(200); + expect(response4.body.field.type).to.be('string'); + }); + + it('can add a new runtime field', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "doc['field_name'].value", + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeBar' + ); + + expect(response2.status).to.be(200); + expect(typeof response2.body.field.runtimeField).to.be('object'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts new file mode 100644 index 00000000000000..3980821c0fd096 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field/foo`) + .send({ + runtimeField: { + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error when field name is specified', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field/foo`) + .send({ + name: 'foo', + runtimeField: { + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + "[request body.name]: a value wasn't expected to be present" + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/index.ts new file mode 100644 index 00000000000000..f5d556ca9994ab --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts new file mode 100644 index 00000000000000..6b924570a0e45b --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts @@ -0,0 +1,76 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can update an existing field', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + const response2 = await supertest + .post( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field/runtimeFoo` + ) + .send({ + runtimeField: { + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeFoo' + ); + + expect(response3.status).to.be(200); + expect(response3.body.field.type).to.be('string'); + expect(response3.body.field.runtimeField.type).to.be('keyword'); + expect(response3.body.field.runtimeField.script.source).to.be("doc['something_new'].value"); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts index 1210e7247f72dd..663deae1d34769 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts @@ -54,6 +54,7 @@ export default function ({ getService }: FtrProviderContext) { it('newly created scripted field is materialized in the index_pattern object', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, }, diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts index 466af26f6e1272..b9ce3e84d53904 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can remove a scripted field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts index 64d909480260e5..c38c3a01708a74 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can fetch a scripted field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts index 96f5924e7c132f..16b19583068187 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can overwrite an existing field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts index 055b3fe9abe04d..4ffc98e0660bca 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can update an existing field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/kql_telemetry/kql_telemetry.ts b/test/api_integration/apis/kql_telemetry/kql_telemetry.ts index 09e36b9078792a..5770ed0866a900 100644 --- a/test/api_integration/apis/kql_telemetry/kql_telemetry.ts +++ b/test/api_integration/apis/kql_telemetry/kql_telemetry.ts @@ -17,8 +17,16 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('es'); describe('telemetry API', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should increment the opt *in* counter in the .kibana/kql-telemetry document', async () => { await supertest diff --git a/test/api_integration/apis/saved_objects/bulk_create.ts b/test/api_integration/apis/saved_objects/bulk_create.ts index 1f76567e973b22..5867b8125303a5 100644 --- a/test/api_integration/apis/saved_objects/bulk_create.ts +++ b/test/api_integration/apis/saved_objects/bulk_create.ts @@ -39,7 +39,10 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { KIBANA_VERSION = await getKibanaVersion(getService); await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_ID }); - await kibanaServer.importExport.load('saved_objects/basic', { space: SPACE_ID }); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json', + { space: SPACE_ID } + ); }); after(() => kibanaServer.spaces.delete(SPACE_ID)); diff --git a/test/api_integration/apis/saved_objects/bulk_get.ts b/test/api_integration/apis/saved_objects/bulk_get.ts index 81e86913aaf86f..e349482960678b 100644 --- a/test/api_integration/apis/saved_objects/bulk_get.ts +++ b/test/api_integration/apis/saved_objects/bulk_get.ts @@ -34,10 +34,16 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { KIBANA_VERSION = await getKibanaVersion(getService); - await kibanaServer.importExport.load('saved_objects/basic'); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); }); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200 with individual responses', async () => await supertest diff --git a/test/api_integration/apis/saved_objects/bulk_update.ts b/test/api_integration/apis/saved_objects/bulk_update.ts index 8740652fc39538..cf402bf2f62217 100644 --- a/test/api_integration/apis/saved_objects/bulk_update.ts +++ b/test/api_integration/apis/saved_objects/bulk_update.ts @@ -15,8 +15,16 @@ export default function ({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('bulkUpdate', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200', async () => { const response = await supertest diff --git a/test/api_integration/apis/saved_objects/create.ts b/test/api_integration/apis/saved_objects/create.ts index dfa7ceb503dfd4..00018e47c9dd36 100644 --- a/test/api_integration/apis/saved_objects/create.ts +++ b/test/api_integration/apis/saved_objects/create.ts @@ -19,10 +19,16 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { KIBANA_VERSION = await getKibanaVersion(getService); - await kibanaServer.importExport.load('saved_objects/basic'); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); }); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200', async () => { await supertest diff --git a/test/api_integration/apis/saved_objects/delete.ts b/test/api_integration/apis/saved_objects/delete.ts index 9a4525df1b5f75..fc38050deabffe 100644 --- a/test/api_integration/apis/saved_objects/delete.ts +++ b/test/api_integration/apis/saved_objects/delete.ts @@ -14,8 +14,16 @@ export default function ({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('delete', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200 when deleting a doc', async () => await supertest diff --git a/test/api_integration/apis/saved_objects/export.ts b/test/api_integration/apis/saved_objects/export.ts index c42afd4e773c53..6314fbbe675d0e 100644 --- a/test/api_integration/apis/saved_objects/export.ts +++ b/test/api_integration/apis/saved_objects/export.ts @@ -24,7 +24,10 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { KIBANA_VERSION = await getKibanaVersion(getService); await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_ID }); - await kibanaServer.importExport.load('saved_objects/basic', { space: SPACE_ID }); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json', + { space: SPACE_ID } + ); }); after(() => kibanaServer.spaces.delete(SPACE_ID)); diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts index 02fa2d325f1713..a38043c7c93524 100644 --- a/test/api_integration/apis/saved_objects/find.ts +++ b/test/api_integration/apis/saved_objects/find.ts @@ -18,12 +18,18 @@ export default function ({ getService }: FtrProviderContext) { describe('find', () => { before(async () => { await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_ID }); - await kibanaServer.importExport.load('saved_objects/basic', { space: SPACE_ID }); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json', + { space: SPACE_ID } + ); await kibanaServer.spaces.create({ id: `${SPACE_ID}-foo`, name: `${SPACE_ID}-foo` }); - await kibanaServer.importExport.load('saved_objects/basic/foo-ns', { - space: `${SPACE_ID}-foo`, - }); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic/foo-ns.json', + { + space: `${SPACE_ID}-foo`, + } + ); }); after(async () => { @@ -255,10 +261,18 @@ export default function ({ getService }: FtrProviderContext) { }); describe('`has_reference` and `has_reference_operator` parameters', () => { - before(() => kibanaServer.importExport.load('saved_objects/references', { space: SPACE_ID })); - after(() => - kibanaServer.importExport.unload('saved_objects/references', { space: SPACE_ID }) - ); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json', + { space: SPACE_ID } + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json', + { space: SPACE_ID } + ); + }); it('search for a reference', async () => { await supertest @@ -319,12 +333,18 @@ export default function ({ getService }: FtrProviderContext) { }); describe('searching for special characters', () => { - before(() => - kibanaServer.importExport.load('saved_objects/find_edgecases', { space: SPACE_ID }) - ); - after(() => - kibanaServer.importExport.unload('saved_objects/find_edgecases', { space: SPACE_ID }) - ); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/find_edgecases.json', + { space: SPACE_ID } + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/find_edgecases.json', + { space: SPACE_ID } + ); + }); it('can search for objects with dashes', async () => await supertest diff --git a/test/api_integration/apis/saved_objects/get.ts b/test/api_integration/apis/saved_objects/get.ts index 77d7b4faacb414..8122308e449303 100644 --- a/test/api_integration/apis/saved_objects/get.ts +++ b/test/api_integration/apis/saved_objects/get.ts @@ -19,9 +19,15 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { KIBANA_VERSION = await getKibanaVersion(getService); - await kibanaServer.importExport.load('saved_objects/basic'); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); }); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); it('should return 200', async () => await supertest diff --git a/test/api_integration/apis/saved_objects/import.ts b/test/api_integration/apis/saved_objects/import.ts index 8d3c0b7bbcea3f..c899f082ec4d3f 100644 --- a/test/api_integration/apis/saved_objects/import.ts +++ b/test/api_integration/apis/saved_objects/import.ts @@ -43,8 +43,16 @@ export default function ({ getService }: FtrProviderContext) { }; describe('with basic data existing', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 415 when no file passed in', async () => { await supertest diff --git a/test/api_integration/apis/saved_objects/resolve.ts b/test/api_integration/apis/saved_objects/resolve.ts index fcfef0aeb6b582..a00a44f98223d0 100644 --- a/test/api_integration/apis/saved_objects/resolve.ts +++ b/test/api_integration/apis/saved_objects/resolve.ts @@ -22,8 +22,16 @@ export default function ({ getService }: FtrProviderContext) { }); describe('with kibana index', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200', async () => await supertest diff --git a/test/api_integration/apis/saved_objects/resolve_import_errors.ts b/test/api_integration/apis/saved_objects/resolve_import_errors.ts index 43ff01d321f9fa..7ca61a26a11c1e 100644 --- a/test/api_integration/apis/saved_objects/resolve_import_errors.ts +++ b/test/api_integration/apis/saved_objects/resolve_import_errors.ts @@ -36,7 +36,10 @@ export default function ({ getService }: FtrProviderContext) { describe('with basic data existing', () => { before(async () => { await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_ID }); - await kibanaServer.importExport.load('saved_objects/basic', { space: SPACE_ID }); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json', + { space: SPACE_ID } + ); }); after(() => kibanaServer.spaces.delete(SPACE_ID)); diff --git a/test/api_integration/apis/saved_objects/update.ts b/test/api_integration/apis/saved_objects/update.ts index 75b8651ee64a7c..1c73d0788e51ca 100644 --- a/test/api_integration/apis/saved_objects/update.ts +++ b/test/api_integration/apis/saved_objects/update.ts @@ -14,8 +14,16 @@ export default function ({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('update', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200', async () => { await supertest .put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`) diff --git a/test/api_integration/apis/saved_objects_management/find.ts b/test/api_integration/apis/saved_objects_management/find.ts index 6aefd74f0f36b2..6e36303cc1fe07 100644 --- a/test/api_integration/apis/saved_objects_management/find.ts +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -25,8 +25,16 @@ export default function ({ getService }: FtrProviderContext) { }); describe('with kibana index', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200 with individual responses', async () => await supertest @@ -85,8 +93,16 @@ export default function ({ getService }: FtrProviderContext) { }); describe('`hasReference` and `hasReferenceOperator` parameters', () => { - before(() => kibanaServer.importExport.load('saved_objects/references')); - after(() => kibanaServer.importExport.unload('saved_objects/references')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json' + ); + }); it('search for a reference', async () => { await supertest diff --git a/test/api_integration/apis/saved_objects_management/get.ts b/test/api_integration/apis/saved_objects_management/get.ts index eb0b832cb2ed14..3b49a28ca40223 100644 --- a/test/api_integration/apis/saved_objects_management/get.ts +++ b/test/api_integration/apis/saved_objects_management/get.ts @@ -18,8 +18,16 @@ export default function ({ getService }: FtrProviderContext) { const existingObject = 'visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab'; const nonexistentObject = 'wigwags/foo'; - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('should return 200 for object that exists and inject metadata', async () => await supertest diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts index ffc97b13859571..aa488942edbeb7 100644 --- a/test/api_integration/apis/saved_objects_management/relationships.ts +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -43,8 +43,16 @@ export default function ({ getService }: FtrProviderContext) { }); describe('relationships', () => { - before(() => kibanaServer.importExport.load('management/saved_objects/relationships')); - after(() => kibanaServer.importExport.unload('management/saved_objects/relationships')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/management/saved_objects/relationships.json' + ); + }); const baseApiUrl = `/api/kibana/management/saved_objects/relationships`; const defaultTypes = ['visualization', 'index-pattern', 'search', 'dashboard']; diff --git a/test/api_integration/apis/shorten/index.js b/test/api_integration/apis/shorten/index.js index 9af979b6af95d7..86c39426205ad3 100644 --- a/test/api_integration/apis/shorten/index.js +++ b/test/api_integration/apis/shorten/index.js @@ -13,8 +13,16 @@ export default function ({ getService }) { const kibanaServer = getService('kibanaServer'); describe('url shortener', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); it('generates shortened urls', async () => { const resp = await supertest diff --git a/test/api_integration/apis/stats/stats.js b/test/api_integration/apis/stats/stats.js index 2ba14e29a3d1a8..61936a73da38da 100644 --- a/test/api_integration/apis/stats/stats.js +++ b/test/api_integration/apis/stats/stats.js @@ -47,8 +47,16 @@ export default function ({ getService }) { const kibanaServer = getService('kibanaServer'); describe('kibana stats api', () => { - before(() => kibanaServer.importExport.load('saved_objects/basic')); - after(() => kibanaServer.importExport.unload('saved_objects/basic')); + before(async () => { + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json' + ); + }); describe('basic', () => { it('should return the stats without cluster_uuid with no query string params', () => { diff --git a/test/api_integration/apis/suggestions/suggestions.js b/test/api_integration/apis/suggestions/suggestions.js index 526cb9669f2788..292e3f599d81a1 100644 --- a/test/api_integration/apis/suggestions/suggestions.js +++ b/test/api_integration/apis/suggestions/suggestions.js @@ -14,13 +14,17 @@ export default function ({ getService }) { describe('Suggestions API', function () { before(async () => { await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); - await kibanaServer.importExport.load('index_patterns/basic_kibana'); + await kibanaServer.importExport.load( + 'test/api_integration/fixtures/kbn_archiver/index_patterns/basic_kibana.json' + ); }); after(async () => { await esArchiver.unload( 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' ); - await kibanaServer.importExport.unload('index_patterns/basic_kibana'); + await kibanaServer.importExport.unload( + 'test/api_integration/fixtures/kbn_archiver/index_patterns/basic_kibana.json' + ); }); it('should return 200 with special characters', () => diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 63803bd511bd14..f20fa4cafa55e2 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -22,7 +22,6 @@ export function KibanaServerProvider({ getService }: FtrProviderContext): KbnCli url, certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), uiSettingDefaults: defaults, - importExportDir: config.get('kbnArchiver.directory'), }); if (defaults) { diff --git a/test/examples/config.js b/test/examples/config.js index cb6c487c564c3b..d47748e5f22a97 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -28,6 +28,7 @@ export default async function ({ readConfigFile }) { require.resolve('./state_sync'), require.resolve('./routing'), require.resolve('./expressions_explorer'), + require.resolve('./index_pattern_field_editor_example'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/index_pattern_field_editor_example/index.ts b/test/examples/index_pattern_field_editor_example/index.ts new file mode 100644 index 00000000000000..0cd23a33c84762 --- /dev/null +++ b/test/examples/index_pattern_field_editor_example/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const es = getService('es'); + const PageObjects = getPageObjects(['common', 'header', 'settings']); + + describe('index pattern field editor example', function () { + this.tags('ciGroup2'); + before(async () => { + await browser.setWindowSize(1300, 900); + await es.transport.request({ + path: '/blogs/_doc', + method: 'POST', + body: { user: 'matt', message: 20 }, + }); + + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern('blogs', null); + await PageObjects.common.navigateToApp('indexPatternFieldEditorExample'); + }); + + loadTestFile(require.resolve('./index_pattern_field_editor_example')); + }); +} diff --git a/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts b/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts new file mode 100644 index 00000000000000..5744c8e64f5c11 --- /dev/null +++ b/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + + describe('', () => { + it('finds an index pattern', async () => { + await testSubjects.existOrFail('indexPatternTitle'); + }); + it('opens the field editor', async () => { + await testSubjects.click('addField'); + await testSubjects.existOrFail('flyoutTitle'); + }); + }); +} diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts index eaefb3ba38f050..efd97fce3f7f5d 100644 --- a/test/functional/apps/discover/_data_grid.ts +++ b/test/functional/apps/discover/_data_grid.ts @@ -24,7 +24,7 @@ export default function ({ before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index bfce69ca08d552..3d9e01e1dee19d 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -36,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover data grid context tests', () => { before(async () => { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update(defaultSettings); diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts index bbd633df6fa1ed..00e6a5025e2af6 100644 --- a/test/functional/apps/discover/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 8296b518bee32c..94e8e942f86ba2 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update(defaultSettings); diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index f26d1f4536e558..dce6bfba9cd99c 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -29,7 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { log.debug('load kibana index with default index pattern'); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts index d3df79dc8658b6..614a0794ffb3b2 100644 --- a/test/functional/apps/discover/_discover_fields_api.ts +++ b/test/functional/apps/discover/_discover_fields_api.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace(defaultSettings); log.debug('discover'); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index ffe989f8d498ce..b5a40030b26016 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index e8698b984f6cc0..338d17ba31ff49 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 7de123bb44f2a6..110e255d18c756 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { this.tags('includeFirefox'); before(async function () { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', diff --git a/test/functional/apps/discover/_filter_editor.ts b/test/functional/apps/discover/_filter_editor.ts index 4e9839d4e799ae..8bcb4382bb3bf3 100644 --- a/test/functional/apps/discover/_filter_editor.ts +++ b/test/functional/apps/discover/_filter_editor.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/discover/_inspector.ts b/test/functional/apps/discover/_inspector.ts index 9d4fd93eb3a9f0..17f358ec748714 100644 --- a/test/functional/apps/discover/_inspector.ts +++ b/test/functional/apps/discover/_inspector.ts @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts index 3e426c6237d89b..de3f0f2c40ae10 100644 --- a/test/functional/apps/discover/_large_string.ts +++ b/test/functional/apps/discover/_large_string.ts @@ -23,7 +23,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { await security.testUser.setRoles(['kibana_admin', 'kibana_large_strings']); - await kibanaServer.importExport.load('testlargestring'); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/testlargestring.json' + ); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/hamlet'); await kibanaServer.uiSettings.replace({ defaultIndex: 'testlargestring' }); }); diff --git a/test/functional/apps/discover/_saved_queries.ts b/test/functional/apps/discover/_saved_queries.ts index 67806c9d16a940..20f2cab907d9bf 100644 --- a/test/functional/apps/discover/_saved_queries.ts +++ b/test/functional/apps/discover/_saved_queries.ts @@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async function () { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); diff --git a/test/functional/apps/discover/_shared_links.ts b/test/functional/apps/discover/_shared_links.ts index 151e8555eea77e..62364739db311b 100644 --- a/test/functional/apps/discover/_shared_links.ts +++ b/test/functional/apps/discover/_shared_links.ts @@ -38,7 +38,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('load kibana index with default index pattern'); await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); - await kibanaServer.importExport.load('discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/visualize/legacy/index.ts b/test/functional/apps/visualize/legacy/index.ts index 4e43ec90d16999..d474287860b25e 100644 --- a/test/functional/apps/visualize/legacy/index.ts +++ b/test/functional/apps/visualize/legacy/index.ts @@ -23,7 +23,7 @@ export default function ({ getPageObjects, getService, loadTestFile }: FtrProvid await browser.setWindowSize(1280, 800); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/long_window_logstash'); - await kibanaServer.importExport.load('visualize'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index f77db553e015f9..a11a254509e7a8 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -49,7 +49,9 @@ export class VisualizePageObject extends FtrService { public async initTests(isNewLibrary = false) { await this.kibanaServer.savedObjects.clean({ types: ['visualization'] }); - await this.kibanaServer.importExport.load('visualize'); + await this.kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/visualize.json' + ); await this.kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', diff --git a/x-pack/examples/reporting_example/public/components/app.tsx b/x-pack/examples/reporting_example/public/components/app.tsx index 0174ec2a17ad4d..5f6f5d17afafba 100644 --- a/x-pack/examples/reporting_example/public/components/app.tsx +++ b/x-pack/examples/reporting_example/public/components/app.tsx @@ -6,8 +6,9 @@ */ import { + EuiButton, EuiCard, - EuiCode, + EuiContextMenu, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -17,7 +18,7 @@ import { EuiPageContent, EuiPageContentBody, EuiPageHeader, - EuiPanel, + EuiPopover, EuiText, EuiTitle, } from '@elastic/eui'; @@ -50,7 +51,19 @@ export const ReportingExampleApp = ({ reporting, screenshotMode, }: ReportingExampleAppDeps) => { - const { getDefaultLayoutSelectors, ReportingAPIClient } = reporting; + const { getDefaultLayoutSelectors } = reporting; + + // Context Menu + const [isPopoverOpen, setPopover] = useState(false); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + // Async Logos const [logos, setLogos] = useState([]); useEffect(() => { @@ -61,7 +74,7 @@ export const ReportingExampleApp = ({ }); }); - const getPDFJobParams = (): JobParamsPDF => { + const getPDFJobParamsDefault = (): JobParamsPDF => { return { layout: { id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, @@ -73,7 +86,40 @@ export const ReportingExampleApp = ({ }; }; - // Render the application DOM. + const panels = [ + { id: 0, items: [{ name: 'PDF Reports', icon: 'document', panel: 1 }] }, + { + id: 1, + initialFocusedItemIndex: 1, + title: 'PDF Reports', + items: [ + { name: 'No Layout Option', icon: 'document', panel: 2 }, + { name: 'Canvas Layout Option', icon: 'canvasApp', panel: 3 }, + ], + }, + { + id: 2, + title: 'No Layout Option', + content: ( + + ), + }, + { + id: 3, + title: 'Canvas Layout Option', + content: ( + + ), + }, + ]; + return ( @@ -87,34 +133,21 @@ export const ReportingExampleApp = ({ -

- Use the ReportingStart.components.ScreenCapturePanel{' '} - component to add the Reporting panel to your page. -

- - +

Example of a Sharing menu using components from Reporting

- - - - - - - + Share} + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + -

- The logos below are in a data-shared-items-container element - for Reporting. -

-
{logos.map((item, index) => ( diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index c157765afb3590..6847b17bcef4b8 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -37,7 +37,7 @@ import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; import { UntypedNormalizedAlertType } from '../alert_type_registry'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; -import uuid from 'uuid'; + const alertType: jest.Mocked = { id: 'test', name: 'My test alert', @@ -373,6 +373,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -395,6 +397,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -525,6 +529,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -546,6 +552,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -669,7 +677,11 @@ describe('Task Runner', () => { meta: { lastScheduledActions: { date: '1970-01-01T00:00:00.000Z', group: 'default' }, }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 86400000000000, + }, }, }, }, @@ -699,6 +711,8 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -924,17 +938,221 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '1'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute-action", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "action_subgroup": undefined, + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + Object { + "id": "1", + "namespace": undefined, + "type": "action", + "type_id": "action", + }, + ], + }, + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + const runnerResult = await taskRunner.run(); + expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "lastScheduledActions": Object { + "date": 1970-01-01T00:00:00.000Z, + "group": "default", + "subgroup": undefined, + }, + }, + "state": Object { + "bar": false, + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", + }, + }, + } + `); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` + ); + expect(logger.debug).nthCalledWith( + 4, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { "event": Object { - "action": "new-instance", + "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { "alerting": Object { - "action_group_id": "default", - "instance_id": "1", + "instance_id": "2", }, "saved_objects": Array [ Object { @@ -946,13 +1164,15 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' created new instance: '1'", + "message": "test:1: 'alert-name' instance '2' has recovered", }, ], Array [ Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -972,6 +1192,36 @@ describe('Task Runner', () => { "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], + Array [ + Object { + "event": Object { + "action": "execute-action", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "recovered", + "action_subgroup": undefined, + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + Object { + "id": "2", + "namespace": undefined, + "type": "action", + "type_id": "action", + }, + ], + }, + "message": "alert: test:1: 'alert-name' instanceId: '2' scheduled actionGroup: 'recovered' action: action:2", + }, + ], Array [ Object { "event": Object { @@ -1028,84 +1278,7 @@ describe('Task Runner', () => { ], ] `); - }); - - test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { - taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); - taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); - - alertType.executor.mockImplementation( - async ({ - services: executorServices, - }: AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - string - >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - } - ); - const taskRunner = new TaskRunner( - alertType, - { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - alertInstances: { - '1': { meta: {}, state: { bar: false } }, - '2': { meta: {}, state: { bar: false } }, - }, - }, - }, - taskRunnerFactoryInitializerParams - ); - alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - }, - }, - } - `); - - const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); - expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); - expect(logger.debug).nthCalledWith( - 2, - `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` - ); - expect(logger.debug).nthCalledWith( - 3, - `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` - ); - expect(logger.debug).nthCalledWith( - 4, - 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' - ); - const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1129,7 +1302,7 @@ describe('Task Runner', () => { }); test('should skip alertInstances which werent active on the previous execution', async () => { - const alertId = uuid.v4(); + const alertId = 'e558aaad-fd81-46d2-96fc-3bd8fc3dc03f'; taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -1344,11 +1517,19 @@ describe('Task Runner', () => { alertInstances: { '1': { meta: { lastScheduledActions: { group: 'default', date } }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, }, '2': { meta: { lastScheduledActions: { group: 'default', date } }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, }, }, }, @@ -1377,6 +1558,8 @@ describe('Task Runner', () => { }, "state": Object { "bar": false, + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, }, } @@ -1390,6 +1573,9 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -1413,6 +1599,8 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -2035,9 +2223,759 @@ describe('Task Runner', () => { references: [], }); + const logger = taskRunnerFactoryInitializerParams.logger; return taskRunner.run().catch((ex) => { expect(ex).toMatchInlineSnapshot(`[Error: Saved object [alert/1] not found]`); + expect(logger.debug).toHaveBeenCalledWith( + `Executing Alert "1" has resulted in Error: Saved object [alert/1] not found` + ); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).nthCalledWith( + 1, + `Unable to execute rule "1" because Saved object [alert/1] not found - this rule will not be rescheduled. To restart rule execution, try disabling and re-enabling this rule.` + ); expect(isUnrecoverableError(ex)).toBeTruthy(); }); }); + + test('correctly logs warning when Alert Task Runner throws due to failing to fetch the alert in a space', async () => { + alertsClient.get.mockImplementation(() => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1'); + }); + + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + params: { + ...mockedTaskInstance.params, + spaceId: 'test space', + }, + }, + taskRunnerFactoryInitializerParams + ); + + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const logger = taskRunnerFactoryInitializerParams.logger; + return taskRunner.run().catch((ex) => { + expect(ex).toMatchInlineSnapshot(`[Error: Saved object [alert/1] not found]`); + expect(logger.debug).toHaveBeenCalledWith( + `Executing Alert "1" has resulted in Error: Saved object [alert/1] not found` + ); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).nthCalledWith( + 1, + `Unable to execute rule "1" in the "test space" space because Saved object [alert/1] not found - this rule will not be rescheduled. To restart rule execution, try disabling and re-enabling this rule.` + ); + }); + }); + + test('start time is logged for new alerts', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: {}, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '1'", + }, + ], + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '2'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('duration is updated for active alerts when alert state contains start time', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 64800000000000, + "start": "1969-12-31T06:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('duration is not calculated for active alerts when alert state does not contain start time', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { bar: false }, + }, + '2': { + meta: {}, + state: { bar: false }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('end is logged for active alerts when alert state contains start time and alert recovers', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation(async () => {}); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "recovered-instance", + "duration": 86400000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '1' has recovered", + }, + ], + Array [ + Object { + "event": Object { + "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '2' has recovered", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "ok", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('end calculation is skipped for active alerts when alert state does not contain start time and alert recovers', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => {} + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { bar: false }, + }, + '2': { + meta: {}, + state: { bar: false }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "recovered-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '1' has recovered", + }, + ], + Array [ + Object { + "event": Object { + "action": "recovered-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '2' has recovered", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "ok", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index fd82b38b493d79..4a214efceaa7d7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -323,6 +323,12 @@ export class TaskRunner< alertLabel, }); + trackAlertDurations({ + originalAlerts: originalAlertInstances, + currentAlerts: instancesWithScheduledActions, + recoveredAlerts: recoveredAlertInstances, + }); + generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, @@ -581,6 +587,10 @@ export class TaskRunner< ), schedule: resolveErr(schedule, (error) => { if (isAlertSavedObjectNotFoundError(error, alertId)) { + const spaceMessage = spaceId ? `in the "${spaceId}" space ` : ''; + this.logger.warn( + `Unable to execute rule "${alertId}" ${spaceMessage}because ${error.message} - this rule will not be rescheduled. To restart rule execution, try disabling and re-enabling this rule.` + ); throwUnrecoverableError(error); } return { interval: taskSchedule?.interval ?? FALLBACK_RETRY_INTERVAL }; @@ -589,6 +599,61 @@ export class TaskRunner< } } +interface TrackAlertDurationsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +> { + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; +} + +function trackAlertDurations< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +>(params: TrackAlertDurationsParams) { + const currentTime = new Date().toISOString(); + const { currentAlerts, originalAlerts, recoveredAlerts } = params; + const originalAlertIds = Object.keys(originalAlerts); + const currentAlertIds = Object.keys(currentAlerts); + const recoveredAlertIds = Object.keys(recoveredAlerts); + const newAlertIds = without(currentAlertIds, ...originalAlertIds); + + // Inject start time into instance state of new instances + for (const id of newAlertIds) { + const state = currentAlerts[id].getState(); + currentAlerts[id].replaceState({ ...state, start: currentTime }); + } + + // Calculate duration to date for active instances + for (const id of currentAlertIds) { + const state = originalAlertIds.includes(id) + ? originalAlerts[id].getState() + : currentAlerts[id].getState(); + const duration = state.start + ? (new Date(currentTime).valueOf() - new Date(state.start as string).valueOf()) * 1000 * 1000 // nanoseconds + : undefined; + currentAlerts[id].replaceState({ + ...state, + ...(state.start ? { start: state.start } : {}), + ...(duration !== undefined ? { duration } : {}), + }); + } + + // Inject end time into instance state of recovered instances + for (const id of recoveredAlertIds) { + const state = recoveredAlerts[id].getState(); + const duration = state.start + ? (new Date(currentTime).valueOf() - new Date(state.start as string).valueOf()) * 1000 * 1000 // nanoseconds + : undefined; + recoveredAlerts[id].replaceState({ + ...state, + ...(duration ? { duration } : {}), + ...(state.start ? { end: currentTime } : {}), + }); + } +} + interface GenerateNewAndRecoveredInstanceEventsParams< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext @@ -624,38 +689,66 @@ function generateNewAndRecoveredInstanceEvents< for (const id of recoveredAlertInstanceIds) { const { group: actionGroup, subgroup: actionSubgroup } = recoveredAlertInstances[id].getLastScheduledActions() ?? {}; + const state = recoveredAlertInstances[id].getState(); const message = `${params.alertLabel} instance '${id}' has recovered`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.recoveredInstance, + message, + state, + actionGroup, + actionSubgroup + ); } for (const id of newIds) { const { actionGroup, subgroup: actionSubgroup } = currentAlertInstances[id].getScheduledActionOptions() ?? {}; + const state = currentAlertInstances[id].getState(); const message = `${params.alertLabel} created new instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.newInstance, + message, + state, + actionGroup, + actionSubgroup + ); } for (const id of currentAlertInstanceIds) { const { actionGroup, subgroup: actionSubgroup } = currentAlertInstances[id].getScheduledActionOptions() ?? {}; + const state = currentAlertInstances[id].getState(); const message = `${params.alertLabel} active instance: '${id}' in ${ actionSubgroup ? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'` : `actionGroup: '${actionGroup}'` }`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.activeInstance, + message, + state, + actionGroup, + actionSubgroup + ); } function logInstanceEvent( instanceId: string, action: string, message: string, + state: InstanceState, group?: string, subgroup?: string ) { const event: IEvent = { event: { action, + ...(state?.start ? { start: state.start as string } : {}), + ...(state?.end ? { end: state.end as string } : {}), + ...(state?.duration !== undefined ? { duration: state.duration as number } : {}), }, kibana: { alerting: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 71355a84d28d49..d71751805ce416 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -5,14 +5,7 @@ * 2.0. */ -import { - EuiTitle, - EuiSpacer, - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { isString } from 'lodash'; @@ -100,17 +93,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { ); return ( - - -

- {i18n.translate('xpack.apm.agentConfig.servicePage.title', { - defaultMessage: 'Choose service', - })} -

-
- - - + <> {/* Service name options */}
- + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx index b26779c5fe9491..7623e467aaa2d1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx @@ -15,11 +15,10 @@ import { EuiForm, EuiHealth, EuiLoadingSpinner, - EuiPanel, EuiSpacer, EuiStat, EuiText, - EuiTitle, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo, useState } from 'react'; @@ -136,79 +135,55 @@ export function SettingsPage({ }} > {/* Selected Service panel */} - - -

- {i18n.translate('xpack.apm.agentConfig.chooseService.title', { - defaultMessage: 'Choose service', - })} -

-
- - - - - - - - - - - {!isEditMode && ( - - {i18n.translate( - 'xpack.apm.agentConfig.chooseService.editButton', - { defaultMessage: 'Edit' } - )} - + + + + + + - -
+ /> + + + {!isEditMode && ( + + {i18n.translate( + 'xpack.apm.agentConfig.chooseService.editButton', + { defaultMessage: 'Edit' } + )} + + )} + + - + {/* Settings panel */} - - -

- {i18n.translate('xpack.apm.agentConfig.settings.title', { - defaultMessage: 'Configuration options', - })} -

-
- - - {isLoading ? ( -
- -
- ) : ( - renderSettings({ unsavedChanges, newConfig, setNewConfig }) - )} -
+ {isLoading ? ( +
+ +
+ ) : ( + renderSettings({ unsavedChanges, newConfig, setNewConfig }) + )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 07afb2fece283e..a0ca7daf826106 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -109,7 +109,15 @@ export function AgentConfigurationCreateEdit({ return ( <> - + + {i18n.translate('xpack.apm.agentConfig.newConfig.description', { + defaultMessage: `Fine-tune your agent configuration from within the APM app. Changes are automatically propagated to your APM agents, so there’s no need to redeploy.`, + })} + + + + +

{isEditMode ? i18n.translate('xpack.apm.agentConfig.editConfigTitle', { @@ -121,12 +129,6 @@ export function AgentConfigurationCreateEdit({

- - {i18n.translate('xpack.apm.agentConfig.newConfig.description', { - defaultMessage: `Fine-tune your agent configuration from within the APM app. Changes are automatically propagated to your APM agents, so there’s no need to redeploy.`, - })} - - {pageStep === 'choose-service-step' && ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index b781a6569cc359..1ca7f46a0b26fa 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -10,7 +10,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiSpacer, EuiTitle, EuiText, @@ -42,44 +41,36 @@ export function AgentConfigurations() { return ( <> - -

- {i18n.translate('xpack.apm.agentConfig.titleText', { - defaultMessage: 'Agent central configuration', - })} -

-
- {i18n.translate('xpack.apm.settings.agentConfig.descriptionText', { defaultMessage: `Fine-tune your agent configuration from within the APM app. Changes are automatically propagated to your APM agents, so there’s no need to redeploy.`, })} - - - - - -

- {i18n.translate( - 'xpack.apm.agentConfig.configurationsPanelTitle', - { defaultMessage: 'Configurations' } - )} -

-
-
- {hasConfigurations ? : null} -
+ - + + + +

+ {i18n.translate( + 'xpack.apm.agentConfig.configurationsPanelTitle', + { defaultMessage: 'Configurations' } + )} +

+
+
+ + {hasConfigurations ? : null} +
+ + - -
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx index 28cb4ebd51cddc..1b19bb5860b2c7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -26,7 +26,7 @@ describe('ApmIndices', () => { expect(getByText('Indices')).toMatchInlineSnapshot(`

Indices

diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 44a3c4655417cf..2d74187f9d83b9 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -13,7 +13,6 @@ import { EuiFlexItem, EuiForm, EuiFormRow, - EuiPanel, EuiSpacer, EuiText, EuiTitle, @@ -176,100 +175,101 @@ export function ApmIndices() { return ( <> - + + {i18n.translate('xpack.apm.settings.apmIndices.description', { + defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, + })} + + + + +

{i18n.translate('xpack.apm.settings.apmIndices.title', { defaultMessage: 'Indices', })}

- - - {i18n.translate('xpack.apm.settings.apmIndices.description', { - defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, - })} - - - - - - - {APM_INDEX_LABELS.map(({ configurationName, label }) => { - const matchedConfiguration = data.apmIndexSettings.find( - ({ configurationName: configName }) => - configName === configurationName - ); - const defaultValue = matchedConfiguration - ? matchedConfiguration.defaultValue - : ''; - const savedUiIndexValue = apmIndices[configurationName] || ''; - return ( - + + + + + {APM_INDEX_LABELS.map(({ configurationName, label }) => { + const matchedConfiguration = data.apmIndexSettings.find( + ({ configurationName: configName }) => + configName === configurationName + ); + const defaultValue = matchedConfiguration + ? matchedConfiguration.defaultValue + : ''; + const savedUiIndexValue = apmIndices[configurationName] || ''; + return ( + + + + ); + })} + + + + + {i18n.translate( + 'xpack.apm.settings.apmIndices.cancelButton', + { defaultMessage: 'Cancel' } + )} + + + + + - - - ); - })} - - - - {i18n.translate( - 'xpack.apm.settings.apmIndices.cancelButton', - { defaultMessage: 'Cancel' } + 'xpack.apm.settings.apmIndices.applyButton', + { defaultMessage: 'Apply changes' } )} - - - - - - {i18n.translate( - 'xpack.apm.settings.apmIndices.applyButton', - { defaultMessage: 'Apply changes' } - )} - - - - - - - - - + + + + + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index ab18a31e769172..c1315f165abdb2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -8,7 +8,6 @@ import { EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiTitle, EuiText, EuiSpacer, @@ -83,62 +82,51 @@ export function CustomLinkOverview() { }} /> )} - - - - + + + {i18n.translate('xpack.apm.settings.customizeUI.customLink.info', { + defaultMessage: + 'These links will be shown in the Actions context menu in selected areas of the app, e.g. by the transactions detail.', + })} + + + + + + + +

+ {i18n.translate('xpack.apm.settings.customizeUI.customLink', { + defaultMessage: 'Custom Links', + })} +

+
+
+ {hasValidLicense && !showEmptyPrompt && ( + + - - - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink', - { - defaultMessage: 'Custom Links', - } - )} -

-
-
-
+
- {hasValidLicense && !showEmptyPrompt && ( - - - - - - - - )} -
- - - {i18n.translate('xpack.apm.settings.customizeUI.customLink.info', { - defaultMessage: - 'These links will be shown in the Actions context menu in selected areas of the app, e.g. by the transactions detail.', - })} - - {hasValidLicense ? ( - showEmptyPrompt ? ( - - ) : ( - - ) - ) : ( - )} -
+ + + + + {hasValidLicense ? ( + showEmptyPrompt ? ( + + ) : ( + + ) + ) : ( + + )} ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx index c4b3c39248ffbf..9ce1f1325bb2c4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx @@ -6,28 +6,8 @@ */ import React from 'react'; -import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { CustomLinkOverview } from './CustomLink'; export function CustomizeUI() { - return ( - <> - -

- {i18n.translate('xpack.apm.settings.customizeApp.title', { - defaultMessage: 'Customize app', - })} -

-
- - - {i18n.translate('xpack.apm.settings.customizeApp.description', { - defaultMessage: `Extend the APM app experience with the following settings.`, - })} - - - - - ); + return ; } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index bf9062418313ad..89e5dfcbdadfb7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -7,7 +7,6 @@ import React, { useState } from 'react'; import { - EuiPanel, EuiTitle, EuiText, EuiSpacer, @@ -70,29 +69,27 @@ export function AddEnvironments({ if (!canCreateJob) { return ( - - {ML_ERRORS.MISSING_WRITE_PRIVILEGES}} - /> - + {ML_ERRORS.MISSING_WRITE_PRIVILEGES}} + /> ); } const isLoading = status === FETCH_STATUS.LOADING; return ( - - + <> +

{i18n.translate( 'xpack.apm.settings.anomalyDetection.addEnvironments.titleText', - { - defaultMessage: 'Select environments', - } + { defaultMessage: 'Select environments' } )}

+ + {i18n.translate( 'xpack.apm.settings.anomalyDetection.addEnvironments.descriptionText', @@ -181,6 +178,6 @@ export function AddEnvironments({ -
+ ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 38b9970f64d32c..57d141d763909b 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -6,8 +6,6 @@ */ import React, { useState } from 'react'; -import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; @@ -66,20 +64,6 @@ export function AnomalyDetection() { return ( <> - -

- {i18n.translate('xpack.apm.settings.anomalyDetection.titleText', { - defaultMessage: 'Anomaly detection', - })} -

-
- - - {i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', { - defaultMessage: `Machine Learning's anomaly detection integration enables application health status indicators for services in each configured environment by identifying anomalies in latency.`, - })} - - {viewAddEnvironments ? ( environment)} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 9c69d692876b0a..6df2ed73afe76a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -9,7 +9,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiSpacer, EuiText, EuiTitle, @@ -66,7 +65,28 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { const { jobs, hasLegacyJobs } = data; return ( - + <> + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText', + { + defaultMessage: 'Machine Learning', + } + )} + + ), + }} + /> + + + + @@ -91,25 +111,9 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - - - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText', - { - defaultMessage: 'Machine Learning', - } - )} - - ), - }} - /> - - + + + {hasLegacyJobs && } - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx new file mode 100644 index 00000000000000..c6394f09b0d3cf --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { getRedirectToTransactionDetailPageUrl } from '../TraceLink/get_redirect_to_transaction_detail_page_url'; + +const CentralizedContainer = euiStyled.div` + height: 100%; + display: flex; +`; + +export function TransactionLink({ + match, +}: RouteComponentProps<{ transactionId: string }>) { + const { transactionId } = match.params; + const { urlParams } = useUrlParams(); + const { rangeFrom, rangeTo } = urlParams; + + const { data = { transaction: null }, status } = useFetcher( + (callApmApi) => { + if (transactionId) { + return callApmApi({ + endpoint: 'GET /api/apm/transactions/{transactionId}', + params: { + path: { + transactionId, + }, + }, + }); + } + }, + [transactionId] + ); + if (transactionId && status === FETCH_STATUS.SUCCESS) { + if (data.transaction) { + return ( + + ); + } + + return ; + } + + return ( + + Fetching transaction...} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 09c25ee4557c56..36580d38e660da 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -20,6 +20,7 @@ import { AnomalyDetection } from '../app/Settings/anomaly_detection'; import { ApmIndices } from '../app/Settings/ApmIndices'; import { CustomizeUI } from '../app/Settings/CustomizeUI'; import { TraceLink } from '../app/TraceLink'; +import { TransactionLink } from '../app/transaction_link'; import { TransactionDetails } from '../app/transaction_details'; import { enableServiceOverview } from '../../../common/ui_settings_keys'; import { redirectTo } from './redirect_to'; @@ -219,41 +220,33 @@ function TransactionDetailsRouteView( function SettingsAgentConfigurationRouteView() { return ( - - - - - + + + ); } function SettingsAnomalyDetectionRouteView() { return ( - - - - - + + + ); } function SettingsApmIndicesRouteView() { return ( - - - - - + + + ); } function SettingsCustomizeUI() { return ( - - - - - + + + ); } @@ -276,14 +269,12 @@ export function EditAgentConfigurationRouteView(props: RouteComponentProps) { ); return ( - - - - - + + + ); } @@ -295,13 +286,11 @@ export function CreateAgentConfigurationRouteView(props: RouteComponentProps) { const { pageStep } = toQuery(search); return ( - - - - - + + + ); } @@ -510,6 +499,12 @@ export const apmRouteConfig: APMRouteDefinition[] = [ component: TraceLink, breadcrumb: null, }, + { + exact: true, + path: '/link-to/transaction/:transactionId', + component: TransactionLink, + breadcrumb: null, + }, ]; function RedirectToDefaultServiceRouteView( diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index e917350f6024bf..357ba4c3d9f386 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -31,6 +31,7 @@ export function ApmMainTemplate({ children: React.ReactNode; } & EuiPageTemplateProps) { const { services } = useKibana(); + const ObservabilityPageTemplate = services.observability.navigation.PageTemplate; diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx index ab53052780ea9a..bbaffbd3374c6c 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx @@ -33,23 +33,21 @@ import { Correlations } from '../../app/correlations'; import { SearchBar } from '../../shared/search_bar'; type Tab = NonNullable[0] & { - key: string; + key: + | 'errors' + | 'metrics' + | 'nodes' + | 'overview' + | 'service-map' + | 'profiling' + | 'transactions'; hidden?: boolean; }; -type TabKey = - | 'errors' - | 'metrics' - | 'nodes' - | 'overview' - | 'service-map' - | 'profiling' - | 'transactions'; - interface Props { children: React.ReactNode; serviceName: string; - selectedTab: TabKey; + selectedTab: Tab['key']; searchBarOptions?: React.ComponentProps; } @@ -107,7 +105,7 @@ function useTabs({ selectedTab, }: { serviceName: string; - selectedTab: TabKey; + selectedTab: Tab['key']; }) { const { agentName, transactionType } = useApmServiceContext(); const { core, config } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx index 36d54525d2537b..d3efef4c883803 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx @@ -11,13 +11,28 @@ import React, { ReactNode } from 'react'; import { SettingsTemplate } from './settings_template'; import { createMemoryHistory } from 'history'; import { MemoryRouter, RouteComponentProps } from 'react-router-dom'; +import { CoreStart } from 'kibana/public'; +import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; const { location } = createMemoryHistory(); +const KibanaReactContext = createKibanaReactContext({ + usageCollection: { reportUiCounter: () => {} }, + observability: { + navigation: { + PageTemplate: () => { + return <>hello world; + }, + }, + }, +} as Partial); + function Wrapper({ children }: { children?: ReactNode }) { return ( - {children} + + {children} + ); } @@ -29,8 +44,8 @@ describe('Settings', () => { } as unknown) as RouteComponentProps<{}>; expect(() => render( - -
+ +
hello world
, { wrapper: Wrapper } ) diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx index 66958c01a7fea1..0e610722a76e7e 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx @@ -5,90 +5,108 @@ * 2.0. */ -import { EuiPage, EuiPageBody, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { EuiPageHeaderProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { ReactNode, useState } from 'react'; +import React from 'react'; +import { History } from 'history'; import { useHistory } from 'react-router-dom'; +import { CoreStart } from 'kibana/public'; +import { ApmMainTemplate } from './apm_main_template'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; -export function SettingsTemplate({ children }: { children: ReactNode }) { +type Tab = NonNullable[0] & { + key: + | 'agent-configurations' + | 'anomaly-detection' + | 'apm-indices' + | 'customize-ui'; + hidden?: boolean; +}; + +interface Props { + children: React.ReactNode; + selectedTab: Tab['key']; +} + +export function SettingsTemplate({ children, selectedTab }: Props) { const { core } = useApmPluginContext(); const history = useHistory(); - const { basePath } = core.http; - const canAccessML = !!core.application.capabilities.ml?.canAccessML; - const { search, pathname } = history.location; + const tabs = getTabs({ history, core, selectedTab }); - const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + return ( + + {children} + + ); +} - const toggleOpenOnMobile = () => { - setisSideNavOpenOnMobile((prevState) => !prevState); - }; +function getTabs({ + history, + core, + selectedTab, +}: { + history: History; + core: CoreStart; + selectedTab: Tab['key']; +}) { + const { basePath } = core.http; + const canAccessML = !!core.application.capabilities.ml?.canAccessML; + const { search } = history.location; - function getSettingsHref(path: string) { - return getAPMHref({ basePath, path: `/settings${path}`, search }); - } + const tabs: Tab[] = [ + { + key: 'agent-configurations', + label: i18n.translate('xpack.apm.settings.agentConfig', { + defaultMessage: 'Agent Configuration', + }), + href: getAPMHref({ + basePath, + path: `/settings/agent-configuration`, + search, + }), + }, + { + key: 'anomaly-detection', + label: i18n.translate('xpack.apm.settings.anomalyDetection', { + defaultMessage: 'Anomaly detection', + }), + href: getAPMHref({ + basePath, + path: `/settings/anomaly-detection`, + search, + }), + hidden: !canAccessML, + }, + { + key: 'customize-ui', + label: i18n.translate('xpack.apm.settings.customizeApp', { + defaultMessage: 'Customize app', + }), + href: getAPMHref({ basePath, path: `/settings/customize-ui`, search }), + }, + { + key: 'apm-indices', + label: i18n.translate('xpack.apm.settings.indices', { + defaultMessage: 'Indices', + }), + href: getAPMHref({ basePath, path: `/settings/apm-indices`, search }), + }, + ]; - return ( - - - toggleOpenOnMobile()} - isOpenOnMobile={isSideNavOpenOnMobile} - items={[ - { - name: i18n.translate('xpack.apm.settings.pageTitle', { - defaultMessage: 'Settings', - }), - id: 0, - items: [ - { - name: i18n.translate('xpack.apm.settings.agentConfig', { - defaultMessage: 'Agent Configuration', - }), - id: '1', - href: getSettingsHref('/agent-configuration'), - isSelected: pathname.startsWith( - '/settings/agent-configuration' - ), - }, - ...(canAccessML - ? [ - { - name: i18n.translate( - 'xpack.apm.settings.anomalyDetection', - { - defaultMessage: 'Anomaly detection', - } - ), - id: '4', - href: getSettingsHref('/anomaly-detection'), - isSelected: pathname === '/settings/anomaly-detection', - }, - ] - : []), - { - name: i18n.translate('xpack.apm.settings.customizeApp', { - defaultMessage: 'Customize app', - }), - id: '3', - href: getSettingsHref('/customize-ui'), - isSelected: pathname === '/settings/customize-ui', - }, - { - name: i18n.translate('xpack.apm.settings.indices', { - defaultMessage: 'Indices', - }), - id: '2', - href: getSettingsHref('/apm-indices'), - isSelected: pathname === '/settings/apm-indices', - }, - ], - }, - ]} - /> - - {children} - - ); + return tabs + .filter((t) => !t.hidden) + .map(({ href, key, label }) => ({ + href, + label, + isSelected: key === selectedTab, + })); } diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 24db9e0cd8504f..bfc0a3daf6f0ed 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -6,7 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { of } from 'rxjs'; +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; import type { ConfigSchema } from '.'; import { AppMountParameters, @@ -86,19 +87,56 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); } - // register observability nav + const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { + defaultMessage: 'Services', + }); + const tracesTitle = i18n.translate('xpack.apm.navigation.tracesTitle', { + defaultMessage: 'Traces', + }); + const serviceMapTitle = i18n.translate( + 'xpack.apm.navigation.serviceMapTitle', + { defaultMessage: 'Service Map' } + ); + + // register observability nav if user has access to plugin plugins.observability.navigation.registerSections( - of([ - { - label: 'APM', - sortKey: 200, - entries: [ - { label: 'Services', app: 'apm', path: '/services' }, - { label: 'Traces', app: 'apm', path: '/traces' }, - { label: 'Service Map', app: 'apm', path: '/service-map' }, - ], - }, - ]) + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.apm.show) { + return [ + // APM navigation + { + label: 'APM', + sortKey: 200, + entries: [ + { label: servicesTitle, app: 'apm', path: '/services' }, + { label: tracesTitle, app: 'apm', path: '/traces' }, + { label: serviceMapTitle, app: 'apm', path: '/service-map' }, + ], + }, + + // UX navigation + { + label: 'User Experience', + sortKey: 201, + entries: [ + { + label: i18n.translate('xpack.apm.ux.overview.heading', { + defaultMessage: 'Overview', + }), + app: 'ux', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + ], + }, + ]; + } + + return []; + }) + ) ); const getApmDataHelper = async () => { @@ -150,26 +188,6 @@ export class ApmPlugin implements Plugin { }, }); - plugins.observability.navigation.registerSections( - of([ - { - label: 'User Experience', - sortKey: 201, - entries: [ - { - label: i18n.translate('xpack.apm.ux.overview.heading', { - defaultMessage: 'Overview', - }), - app: 'ux', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - ], - }, - ]) - ); - core.application.register({ id: 'apm', title: 'APM', @@ -178,29 +196,10 @@ export class ApmPlugin implements Plugin { appRoute: '/app/apm', icon: 'plugins/apm/public/icon.svg', category: DEFAULT_APP_CATEGORIES.observability, - // !! Need to be kept in sync with the routes in x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx deepLinks: [ - { - id: 'services', - title: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { - defaultMessage: 'Services', - }), - path: '/services', - }, - { - id: 'traces', - title: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { - defaultMessage: 'Traces', - }), - path: '/traces', - }, - { - id: 'service-map', - title: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map', - }), - path: '/service-map', - }, + { id: 'services', title: servicesTitle, path: '/services' }, + { id: 'traces', title: tracesTitle, path: '/traces' }, + { id: 'service-map', title: serviceMapTitle, path: '/service-map' }, ], async mount(appMountParameters: AppMountParameters) { diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index b4323ae7f51e28..6987ef07577348 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -21,11 +21,11 @@ export function getTransaction({ setup, }: { transactionId: string; - traceId: string; - setup: Setup & SetupTimeRange; + traceId?: string; + setup: Setup | (Setup & SetupTimeRange); }) { return withApmSpan('get_transaction', async () => { - const { start, end, apmEventClient } = setup; + const { apmEventClient } = setup; const resp = await apmEventClient.search({ apm: { @@ -37,8 +37,8 @@ export function getTransaction({ bool: { filter: asMutableArray([ { term: { [TRANSACTION_ID]: transactionId } }, - { term: { [TRACE_ID]: traceId } }, - ...rangeQuery(start, end), + ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []), + ...('start' in setup ? rangeQuery(setup.start, setup.end) : []), ]), }, }, diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index dd392982b02fd2..7fce04644f2205 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -14,6 +14,7 @@ import { environmentRt, kueryRt, rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { getTransaction } from '../lib/transactions/get_transaction'; const tracesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces', @@ -70,7 +71,24 @@ const rootTransactionByTraceIdRoute = createApmServerRoute({ }, }); +const transactionByIdRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/transactions/{transactionId}', + params: t.type({ + path: t.type({ + transactionId: t.string, + }), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { params } = resources; + const { transactionId } = params.path; + const setup = await setupRequest(resources); + return { transaction: await getTransaction({ transactionId, setup }) }; + }, +}); + export const traceRouteRepository = createApmServerRouteRepository() .add(tracesByIdRoute) .add(tracesRoute) - .add(rootTransactionByTraceIdRoute); + .add(rootTransactionByTraceIdRoute) + .add(transactionByIdRoute); diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index a797a8bda061b6..7a23137e7ef60e 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { BOLD_MD_TOKEN, CANVAS, HTML, JSON, KIBANA, PDF, POST, URL, ZIP } from './constants'; +import { BOLD_MD_TOKEN, CANVAS, HTML, JSON, PDF, URL, ZIP } from './constants'; export const ComponentStrings = { AddEmbeddableFlyout: { @@ -1418,95 +1418,10 @@ export const ComponentStrings = { URL, }, }), - getCopyReportingConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage', { - defaultMessage: 'Copied reporting configuration to clipboard', - }), getCopyShareConfigMessage: () => i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { defaultMessage: 'Copied share markup to clipboard', }), - getExportPDFErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage', { - defaultMessage: "Failed to create {PDF} for '{workpadName}'", - values: { - PDF, - workpadName, - }, - }), - getExportPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFMessage', { - defaultMessage: 'Exporting {PDF}. You can track the progress in Management.', - values: { - PDF, - }, - }), - getExportPDFTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFTitle', { - defaultMessage: "{PDF} export of workpad '{workpadName}'", - values: { - PDF, - workpadName, - }, - }), - getPDFFullPageLayoutHelpText: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.FullPageLayoutHelpText', { - defaultMessage: 'Remove borders and footer logo', - }), - getPDFFullPageLayoutLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.FullPageLayoutLabel', { - defaultMessage: 'Full page layout', - }), - getPDFPanelAdvancedOptionsLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelAdvancedOptionsLabel', { - defaultMessage: 'Advanced options', - }), - getPDFPanelCopyAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel', { - defaultMessage: - 'Alternatively, you can generate a {PDF} from a script or with Watcher by using this {URL}. Press Enter to copy the {URL} to clipboard.', - values: { - PDF, - URL, - }, - }), - getPDFPanelCopyButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel', { - defaultMessage: 'Copy {POST} {URL}', - values: { - POST, - URL, - }, - }), - getPDFPanelCopyDescription: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription', { - defaultMessage: - 'Alternatively, copy this {POST} {URL} to call generation from outside {KIBANA} or from Watcher.', - values: { - POST, - KIBANA, - URL, - }, - }), - getPDFPanelGenerateButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel', { - defaultMessage: 'Generate {PDF}', - values: { - PDF, - }, - }), - getPDFPanelGenerateDescription: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription', { - defaultMessage: - '{PDF}s can take a minute or two to generate based on the size of your workpad.', - values: { - PDF, - }, - }), - getPDFPanelOptionsLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelOptionsLabel', { - defaultMessage: 'Options', - }), getShareableZipErrorTitle: (workpadName: string) => i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { defaultMessage: diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 6213ecb58347c1..5faeaefc9e3922 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -29,7 +29,6 @@ "kibanaUtils", "lens", "maps", - "reporting", "savedObjects", "visualizations" ] diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot index 302e015ea1d3a3..c78ff5bf781eca 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/share_menu.stories.storyshot @@ -1,6 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/WorkpadHeader/ShareMenu default 1`] = ` +exports[`Storyshots components/WorkpadHeader/ShareMenu minimal 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Storyshots components/WorkpadHeader/ShareMenu with Reporting 1`] = `
( -
- 'PDF URL String'} - onCopy={action('onCopy')} - onExport={action('onExport')} - /> -
- )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx index bca96f3851e371..20e52b40bc702d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx @@ -5,19 +5,34 @@ * 2.0. */ -import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; import React from 'react'; +import { platformService } from '../../../../services/stubs/platform'; +import { reportingService } from '../../../../services/stubs/reporting'; import { ShareMenu } from '../share_menu.component'; -storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( +storiesOf('components/WorkpadHeader/ShareMenu', module).add('minimal', () => ( { - action(`getExportUrl('${type}')`); - return type; + /> +)); + +storiesOf('components/WorkpadHeader/ShareMenu', module).add('with Reporting', () => ( + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx deleted file mode 100644 index b412bd5328a5bd..00000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx +++ /dev/null @@ -1,98 +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 React, { useState } from 'react'; -import { - EuiAccordion, - EuiButton, - EuiFormRow, - EuiHorizontalRule, - EuiSpacer, - EuiSwitch, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { Clipboard } from '../../clipboard'; -import { LayoutType } from './utils'; - -import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; - -interface Props { - /** Retrieve URL that will invoke PDF Report generation. */ - getPdfURL: (layout: LayoutType) => string; - /** Handler to invoke when the PDF is exported */ - onExport: (layout: LayoutType) => void; - /** Handler to invoke when the URL is copied to the clipboard. */ - onCopy: () => void; -} - -/** - * A panel displayed in the Export Menu with options in which to generate PDF Reports. - */ -export const PDFPanel = ({ getPdfURL, onExport, onCopy }: Props) => { - const [reportLayout, setReportLayout] = useState('preserve_layout'); - - return ( -
- -

{strings.getPDFPanelGenerateDescription()}

-
- - -
{strings.getPDFPanelOptionsLabel()}
-
- - - - reportLayout === 'canvas' - ? setReportLayout('preserve_layout') - : setReportLayout('canvas') - } - data-test-subj="reportModeToggle" - /> - - onExport(reportLayout)} - size="s" - style={{ width: '100%' }} - data-test-subj="generateReportButton" - > - {strings.getPDFPanelGenerateButtonLabel()} - - - - - -

{strings.getPDFPanelCopyDescription()}

-
- - - - {strings.getPDFPanelCopyButtonLabel()} - - -
-
- ); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx index 0d2e877bebdfd3..d4cb4d0736bb14 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx @@ -5,47 +5,47 @@ * 2.0. */ -import React, { FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; +import { IBasePath } from 'kibana/public'; +import PropTypes from 'prop-types'; +import React, { FunctionComponent, useState } from 'react'; +import { ReportingStart } from '../../../../../reporting/public'; import { ComponentStrings } from '../../../../i18n/components'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { Popover, ClosePopoverFn } from '../../popover'; -import { PDFPanel } from './pdf_panel'; +import { ClosePopoverFn, Popover } from '../../popover'; import { ShareWebsiteFlyout } from './flyout'; -import { LayoutType } from './utils'; +import { CanvasWorkpadSharingData, getPdfJobParams } from './utils'; const { WorkpadHeaderShareMenu: strings } = ComponentStrings; type CopyTypes = 'pdf' | 'reportingConfig'; type ExportTypes = 'pdf' | 'json'; -type ExportUrlTypes = 'pdf'; type CloseTypes = 'share'; export type OnCopyFn = (type: CopyTypes) => void; -export type OnExportFn = (type: ExportTypes, layout?: LayoutType) => void; +export type OnExportFn = (type: ExportTypes) => void; export type OnCloseFn = (type: CloseTypes) => void; -export type GetExportUrlFn = (type: ExportUrlTypes, layout: LayoutType) => string; export interface Props { - /** Flag to include the Reporting option only if Reporting is enabled */ - includeReporting: boolean; - /** Handler to invoke when an export URL is copied to the clipboard. */ - onCopy: OnCopyFn; + /** Canvas workpad to export as PDF **/ + sharingData: CanvasWorkpadSharingData; + sharingServices: { + /** BasePath dependency **/ + basePath: IBasePath; + /** Reporting dependency **/ + reporting?: ReportingStart; + }; /** Handler to invoke when an end product is exported. */ onExport: OnExportFn; - /** Handler to retrive an export URL based on the type of export requested. */ - getExportUrl: GetExportUrlFn; } /** * The Menu for Exporting a Workpad from Canvas. */ export const ShareMenu: FunctionComponent = ({ - includeReporting, - onCopy, + sharingData, + sharingServices: services, onExport, - getExportUrl, }) => { const [showFlyout, setShowFlyout] = useState(false); @@ -53,22 +53,6 @@ export const ShareMenu: FunctionComponent = ({ setShowFlyout(false); }; - const getPDFPanel = (closePopover: ClosePopoverFn) => { - return ( - getExportUrl('pdf', layoutType)} - onExport={(layoutType) => { - onExport('pdf', layoutType); - closePopover(); - }} - onCopy={() => { - onCopy('pdf'); - closePopover(); - }} - /> - ); - }; - const getPanelTree = (closePopover: ClosePopoverFn) => ({ id: 0, items: [ @@ -80,14 +64,20 @@ export const ShareMenu: FunctionComponent = ({ closePopover(); }, }, - includeReporting + services.reporting != null ? { name: strings.getShareDownloadPDFTitle(), icon: 'document', panel: { id: 1, title: strings.getShareDownloadPDFTitle(), - content: getPDFPanel(closePopover), + content: ( + getPdfJobParams(sharingData, services.basePath)} + layoutOption="canvas" + onClose={closePopover} + /> + ), }, 'data-test-subj': 'sharePanel-PDFReports', } @@ -132,8 +122,5 @@ export const ShareMenu: FunctionComponent = ({ }; ShareMenu.propTypes = { - includeReporting: PropTypes.bool.isRequired, - onCopy: PropTypes.func.isRequired, onExport: PropTypes.func.isRequired, - getExportUrl: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts index 47b5e755d439cf..fc4906817cf6fc 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -7,16 +7,12 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../plugins/reporting/public'; -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -import { getWindow } from '../../../lib/get_window'; +import { ComponentStrings } from '../../../../i18n'; +import { CanvasWorkpad, State } from '../../../../types'; import { downloadWorkpad } from '../../../lib/download_workpad'; -import { ShareMenu as Component, Props as ComponentProps } from './share_menu.component'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; import { withServices, WithServicesProps } from '../../../services'; - -import { ComponentStrings } from '../../../../i18n'; +import { getPages, getWorkpad } from '../../../state/selectors/workpad'; +import { Props as ComponentProps, ShareMenu as Component } from './share_menu.component'; const { WorkpadHeaderShareMenu: strings } = ComponentStrings; @@ -25,17 +21,6 @@ const mapStateToProps = (state: State) => ({ pageCount: getPages(state).length, }); -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - interface Props { workpad: CanvasWorkpad; pageCount: number; @@ -45,63 +30,28 @@ export const ShareMenu = compose( connect(mapStateToProps), withServices, withProps( - ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ - includeReporting: services.reporting.includeReporting(), - getExportUrl: (type, layout) => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl( - workpad, - layout, - { pageCount }, - services.platform.getBasePathInterface() - ); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: (type) => { - switch (type) { - case 'pdf': - services.notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - services.notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: (type, layout) => { - switch (type) { - case 'pdf': - return createPdf( - workpad, - layout || 'preserve_layout', - { pageCount }, - services.platform.getBasePathInterface() - ) - .then(({ data }: { data: { job: { id: string } } }) => { - services.notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - services.notify.error(err, { - title: strings.getExportPDFErrorTitle(workpad.name), - 'data-test-subj': 'queueReportError', - }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) + ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => { + const { + platform, + reporting: { start: reporting }, + } = services; + + return { + sharingServices: { basePath: platform.getBasePathInterface(), reporting }, + sharingData: { workpad, pageCount }, + onExport: (type) => { + switch (type) { + case 'pdf': + // notifications are automatically handled by the Reporting plugin + break; + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }; + } ) )(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts index df82feb088379f..fd6f4bb894991c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts @@ -7,9 +7,8 @@ jest.mock('../../../../common/lib/fetch'); -import { getPdfUrl, createPdf, LayoutType } from './utils'; +import { getPdfJobParams } from './utils'; import { workpads } from '../../../../__fixtures__/workpads'; -import { fetch } from '../../../../common/lib/fetch'; import { IBasePath } from 'kibana/public'; const basePath = ({ @@ -17,33 +16,36 @@ const basePath = ({ get: () => 'basepath/s/spacey', serverBasePath: `basepath`, } as unknown) as IBasePath; -const workpad = workpads[0]; - -test('getPdfUrl returns the correct url for canvas layout', () => { - ['canvas', 'preserve_layout'].forEach((layout) => { - const url = getPdfUrl(workpad, layout as LayoutType, { pageCount: 2 }, basePath); - - expect(url).toMatchInlineSnapshot( - `"basepath/s/spacey//api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FNew_York,layout:(dimensions:(height:0,width:0),id:${layout}),objectType:'canvas%20workpad',relativeUrls:!(%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F1,%2Fs%2Fspacey%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fbase-workpad%2Fpage%2F2),title:'base%20workpad')"` - ); - }); -}); - -test('createPdf posts to create the pdf with canvas layout', () => { - ['canvas', 'preserve_layout'].forEach((layout, index) => { - createPdf(workpad, layout as LayoutType, { pageCount: 2 }, basePath); - - expect(fetch.post).toBeCalled(); - - const args = (fetch.post as jest.MockedFunction).mock.calls[index]; - - expect(args[0]).toMatchInlineSnapshot( - `"basepath/s/spacey//api/reporting/generate/printablePdf"` - ); - expect(args[1]).toMatchInlineSnapshot(` - Object { - "jobParams": "(browserTimezone:America/New_York,layout:(dimensions:(height:0,width:0),id:${layout}),objectType:'canvas workpad',relativeUrls:!(/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/1,/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/2),title:'base workpad')", - } - `); - }); +const workpadSharingData = { workpad: workpads[0], pageCount: 12 }; + +test('getPdfJobParams returns the correct job params for canvas layout', () => { + const jobParams = getPdfJobParams(workpadSharingData, basePath); + expect(jobParams).toMatchInlineSnapshot(` + Object { + "browserTimezone": "America/New_York", + "layout": Object { + "dimensions": Object { + "height": 0, + "width": 0, + }, + "id": "canvas", + }, + "objectType": "canvas workpad", + "relativeUrls": Array [ + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/1", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/2", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/3", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/4", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/5", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/6", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/7", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/8", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/9", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/10", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/11", + "/s/spacey/app/canvas#/export/workpad/pdf/base-workpad/page/12", + ], + "title": "base workpad", + } + `); }); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts index 40797ef0977819..9586242bc7386f 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts @@ -5,33 +5,24 @@ * 2.0. */ -import rison from 'rison-node'; import { IBasePath } from 'kibana/public'; import moment from 'moment-timezone'; -import { fetch } from '../../../../common/lib/fetch'; +import rison from 'rison-node'; +import { BaseParams } from '../../../../../reporting/common/types'; import { CanvasWorkpad } from '../../../../types'; -import { url } from '../../../../../../../src/plugins/kibana_utils/public'; -interface PageCount { +export interface CanvasWorkpadSharingData { + workpad: Pick; pageCount: number; } -export type LayoutType = 'canvas' | 'preserve_layout'; - -type Arguments = [CanvasWorkpad, LayoutType, PageCount, IBasePath]; - -interface PdfUrlData { - createPdfUri: string; - createPdfPayload: { jobParams: string }; -} +// TODO: get the correct type from Reporting plugin +type JobParamsPDF = BaseParams & { relativeUrls: string[] }; -function getPdfUrlParts( - { id, name: title, width, height }: CanvasWorkpad, - layoutType: LayoutType, - { pageCount }: PageCount, +export function getPdfJobParams( + { workpad: { id, name: title, width, height }, pageCount }: CanvasWorkpadSharingData, basePath: IBasePath -): PdfUrlData { - const reportingEntry = basePath.prepend('/api/reporting/generate'); +): JobParamsPDF { const urlPrefix = basePath.get().replace(basePath.serverBasePath, ''); // for Spaces prefix, which is included in basePath.get() const canvasEntry = `${urlPrefix}/app/canvas#`; @@ -51,34 +42,14 @@ function getPdfUrlParts( workpadUrls.push(rison.encode(`${canvasEntry}/export/workpad/pdf/${id}/page/${i}`)); } - const jobParams = { + return { browserTimezone: moment.tz.guess(), layout: { dimensions: { width, height }, - id: layoutType, + id: 'canvas', }, objectType: 'canvas workpad', relativeUrls: workpadUrls, title, }; - - return { - createPdfUri: `${reportingEntry}/printablePdf`, - createPdfPayload: { - jobParams: rison.encode(jobParams), - }, - }; -} - -export function getPdfUrl(...args: Arguments): string { - const urlParts = getPdfUrlParts(...args); - const param = (key: string, val: any) => - url.encodeUriQuery(key, true) + (val === true ? '' : '=' + url.encodeUriQuery(val, true)); - - return `${urlParts.createPdfUri}?${param('jobParams', urlParts.createPdfPayload.jobParams)}`; -} - -export function createPdf(...args: Arguments) { - const { createPdfUri, createPdfPayload } = getPdfUrlParts(...args); - return fetch.post(createPdfUri, createPdfPayload); } diff --git a/x-pack/plugins/canvas/public/services/reporting.ts b/x-pack/plugins/canvas/public/services/reporting.ts index 3299363cd5c7f1..4fa40401472c65 100644 --- a/x-pack/plugins/canvas/public/services/reporting.ts +++ b/x-pack/plugins/canvas/public/services/reporting.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { ReportingStart } from '../../../reporting/public'; import { CanvasServiceFactory } from './'; export interface ReportingService { - includeReporting: () => boolean; + start?: ReportingStart; } export const reportingServiceFactory: CanvasServiceFactory = ( @@ -18,18 +19,24 @@ export const reportingServiceFactory: CanvasServiceFactory = ( startPlugins ): ReportingService => { const { reporting } = startPlugins; + + const reportingEnabled = () => ({ start: reporting }); + const reportingDisabled = () => ({ start: undefined }); + if (!reporting) { // Reporting is not enabled - return { includeReporting: () => false }; + return reportingDisabled(); } if (reporting.usesUiCapabilities()) { - // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability - return { - includeReporting: () => coreStart.application.capabilities.canvas?.generatePdf === true, - }; + if (coreStart.application.capabilities.canvas?.generatePdf === true) { + // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability + return reportingEnabled(); + } else { + return reportingDisabled(); + } } - // Reporting is enabled as an Elasticsearch feature (Legacy/Deprecated) - return { includeReporting: () => true }; + // Legacy/Deprecated: Reporting is enabled as an Elasticsearch feature + return reportingEnabled(); }; diff --git a/x-pack/plugins/canvas/public/services/stubs/reporting.ts b/x-pack/plugins/canvas/public/services/stubs/reporting.ts index f257dd14543ecd..b32dfc51cbfde8 100644 --- a/x-pack/plugins/canvas/public/services/stubs/reporting.ts +++ b/x-pack/plugins/canvas/public/services/stubs/reporting.ts @@ -8,5 +8,16 @@ import { ReportingService } from '../reporting'; export const reportingService: ReportingService = { - includeReporting: () => true, + start: { + usesUiCapabilities: () => true, + components: { + ReportingPanelPDF: () => (null as unknown) as JSX.Element, + }, + getDefaultLayoutSelectors: () => ({ + screenshot: 'stub', + renderComplete: 'stub', + itemsCountAttribute: 'stub', + timefilterDurationAttribute: 'stub', + }), + }, }; diff --git a/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json b/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json index 113fba55553760..be5bc213e59a4a 100644 --- a/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json +++ b/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json @@ -28,7 +28,7 @@ "height": 510, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" \n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"sum(maleCount) / count(maleCount)\" \n| revealImage origin=\"bottom\" image={asset \"asset-aaa14d64-2c1c-47f2-95c0-21306ee18cba\"} emptyImage={asset \"asset-960c8c6e-da72-412d-9d04-34a98cdb5760\"}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"male_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-aaa14d64-2c1c-47f2-95c0-21306ee18cba\"} emptyImage={asset \"asset-960c8c6e-da72-412d-9d04-34a98cdb5760\"}" }, { "id": "element-4a3fef74-5d8c-4bbe-8f3f-fe55afdd4b60", @@ -50,7 +50,7 @@ "height": 520, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\"\n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0}\n| math \"sum(maleCount) / count(maleCount)\"\n| revealImage origin=\"bottom\" image={asset \"asset-2f64bd10-953d-4163-90e9-a55e9ca4c52a\"} emptyImage={asset \"asset-3a26727a-b756-44be-a82c-273dd85bda09\"}", + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"female_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-2f64bd10-953d-4163-90e9-a55e9ca4c52a\"} emptyImage={asset \"asset-3a26727a-b756-44be-a82c-273dd85bda09\"}", "filter": null }, { @@ -194,7 +194,7 @@ "height": 56, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"femaleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0} | math \"round(100 * sum(femaleCount) / count(femaleCount))\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#eb6c66\" weight=\"normal\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * female_count / total_count)\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#eb6c66\" weight=\"normal\" underline=false italic=false}" }, { "id": "element-9e0b6230-2bc9-4995-8207-043e3063faeb", @@ -205,7 +205,7 @@ "height": 56, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"round(100 * sum(maleCount) / count(maleCount))\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#f8bd4a\" weight=\"normal\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * male_count / total_count)\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#f8bd4a\" weight=\"normal\" underline=false italic=false}" }, { "id": "element-2185edff-ac50-4162-b583-3bfd6469e925", @@ -216,7 +216,7 @@ "height": 721, "angle": 0 }, - "expression": "filters | demodata | markdown \"\" | render containerStyle={containerStyle backgroundColor=\"#ede9e7\"}" + "expression": "markdown \"\" | render containerStyle={containerStyle backgroundColor=\"#ede9e7\"}" }, { "id": "element-71b63f54-0961-4ed2-a85d-45584b48a631", @@ -535,7 +535,7 @@ "height": 202, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" \n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"sum(maleCount) / count(maleCount)\" \n| revealImage origin=\"bottom\" image={asset \"asset-803ec373-2608-4f6f-8cf9-0dbb2f6437ce\"} emptyImage={asset \"asset-18070a2a-cd01-410a-ba89-a4505e2fbc5b\"}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"male_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-803ec373-2608-4f6f-8cf9-0dbb2f6437ce\"} emptyImage={asset \"asset-18070a2a-cd01-410a-ba89-a4505e2fbc5b\"}" }, { "id": "element-2379c3ca-2c31-4948-8412-d14115500efc", @@ -546,7 +546,7 @@ "height": 202, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" \n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0} | math \"sum(maleCount) / count(maleCount)\" \n| revealImage origin=\"bottom\" image={asset \"asset-e644a484-4097-40b9-a08e-7250ba963059\"} emptyImage={asset \"asset-7e4f7119-b2d8-4527-9bd8-887cb25974e7\"}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"female_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-e644a484-4097-40b9-a08e-7250ba963059\"} emptyImage={asset \"asset-7e4f7119-b2d8-4527-9bd8-887cb25974e7\"}" }, { "id": "element-3f52813f-7d0e-4ec7-9aad-c731b670d88d", @@ -964,7 +964,7 @@ "height": 63, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"femaleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0} | math \"round(100 * sum(femaleCount) / count(femaleCount))\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#F05A24\" weight=\"bold\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * female_count / total_count)\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#F05A24\" weight=\"bold\" underline=false italic=false}" }, { "id": "element-86b06b67-893e-4555-ad38-7fba9ea3153b", @@ -975,7 +975,7 @@ "height": 72, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"round(100 * sum(maleCount) / count(maleCount))\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#00A89C\" weight=\"bold\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * male_count / total_count)\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#00A89C\" weight=\"bold\" underline=false italic=false}" }, { "id": "element-507337d9-6e0e-4752-8770-6ebe88e9b3da", diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index f9b7c8b12c2cd1..2a81396025d9af 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -38,6 +38,8 @@ export enum ConnectorTypes { none = '.none', } +export const connectorTypes = Object.values(ConnectorTypes); + const ConnectorJiraTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.jira), fields: rt.union([JiraFieldsRT, rt.null]), diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx index 0a1da1219342ed..3cf01be40eb4ae 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx @@ -50,7 +50,7 @@ describe('Mapping', () => { wrappingComponent: TestProviders, }); expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe( - 'Field mappings require an established connection to ServiceNow ITSM. Please check your connection credentials.' + 'Failed to retrieve mappings for ServiceNow ITSM.' ); }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 2fb2133ba470c1..a379b03a4f675f 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -102,8 +102,7 @@ export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => { export const FIELD_MAPPING_DESC_ERR = (thirdPartyName: string): string => { return i18n.translate('xpack.cases.configureCases.fieldMappingDescErr', { values: { thirdPartyName }, - defaultMessage: - 'Field mappings require an established connection to { thirdPartyName }. Please check your connection credentials.', + defaultMessage: 'Failed to retrieve mappings for { thirdPartyName }.', }); }; export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index dd527122d06168..c232f73c2a2351 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -25,6 +25,7 @@ import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } fro import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; +import { casesConnectors } from '../../connectors'; /** * Returns true if the case should be closed based on the configuration settings and whether the case @@ -110,8 +111,7 @@ export const push = async ( }); const connectorMappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector.id, - connectorType: connector.actionTypeId, + connector: theCase.connector, }); if (connectorMappings.length === 0) { @@ -125,6 +125,7 @@ export const push = async ( connector: connector as ActionConnector, mappings: connectorMappings[0].attributes.mappings, alerts, + casesConnectors, }); const pushRes = await actionsClient.execute({ diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 9f18fa4931e62e..bfd5d1279420be 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -30,6 +30,7 @@ import { } from './utils'; import { flattenCaseSavedObject } from '../../common'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { casesConnectors } from '../../connectors'; const formatComment = { commentId: commentObj.id, @@ -443,6 +444,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res).toEqual({ @@ -471,6 +473,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res.comments).toEqual([ @@ -501,6 +504,7 @@ describe('utils', () => { }, ], alerts: [], + casesConnectors, }); expect(res.comments).toEqual([]); @@ -531,6 +535,7 @@ describe('utils', () => { }, ], alerts: [], + casesConnectors, }); expect(res.comments).toEqual([ @@ -561,6 +566,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res.comments).toEqual([ @@ -595,6 +601,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }); expect(res).toEqual({ @@ -626,6 +633,7 @@ describe('utils', () => { connector, mappings, alerts: [], + casesConnectors, }).catch((e) => { expect(e).not.toBeNull(); expect(e).toEqual( @@ -645,6 +653,7 @@ describe('utils', () => { connector: { ...connector, actionTypeId: 'not-supported' }, mappings, alerts: [], + casesConnectors, }).catch((e) => { expect(e).not.toBeNull(); expect(e).toEqual(new Error('Invalid external service')); diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index ebcc5a07b4edde..d920c517a00044 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -12,17 +12,15 @@ import { CaseFullExternalService, CaseResponse, CaseUserActionsResponse, - CommentAttributes, - CommentRequestAlertType, - CommentRequestUserType, CommentResponse, CommentResponseAlertsType, CommentType, ConnectorMappingsAttributes, - ConnectorTypes, + CommentAttributes, + CommentRequestUserType, + CommentRequestAlertType, } from '../../../common'; import { ActionsClient } from '../../../../actions/server'; -import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; import { BasicParams, @@ -39,6 +37,7 @@ import { TransformFieldsArgs, } from './types'; import { getAlertIds } from '../utils'; +import { CasesConnectorsMap } from '../../connectors'; interface CreateIncidentArgs { actionsClient: ActionsClient; @@ -47,6 +46,7 @@ interface CreateIncidentArgs { connector: ActionConnector; mappings: ConnectorMappingsAttributes[]; alerts: CasesClientGetAlertsResponse; + casesConnectors: CasesConnectorsMap; } export const getLatestPushInfo = ( @@ -70,9 +70,6 @@ export const getLatestPushInfo = ( return null; }; -const isConnectorSupported = (connectorId: string): connectorId is FormatterConnectorTypes => - Object.values(ConnectorTypes).includes(connectorId as ConnectorTypes); - const getCommentContent = (comment: CommentResponse): string => { if (comment.type === CommentType.user) { return comment.comment; @@ -99,6 +96,7 @@ export const createIncident = async ({ connector, mappings, alerts, + casesConnectors, }: CreateIncidentArgs): Promise => { const { comments: caseComments, @@ -110,20 +108,15 @@ export const createIncident = async ({ updated_by: updatedBy, } = theCase; - if (!isConnectorSupported(connector.actionTypeId)) { - throw new Error('Invalid external service'); - } - const params = { title, description, createdAt, createdBy, updatedAt, updatedBy }; const latestPushInfo = getLatestPushInfo(connector.id, userActions); const externalId = latestPushInfo?.pushedInfo?.external_id ?? null; const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated']; let currentIncident: ExternalServiceParams | undefined; - const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format( - theCase, - alerts - ); + const externalServiceFields = + casesConnectors.get(connector.actionTypeId)?.format(theCase, alerts) ?? {}; + let incident: Partial = { ...externalServiceFields }; if (externalId) { diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index d95667d5eee047..2f486556e4ae54 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -26,7 +26,6 @@ import { excess, GetConfigureFindRequest, GetConfigureFindRequestRt, - GetFieldsResponse, throwErrors, CasesConfigurationsResponse, CaseConfigurationsResponseRt, @@ -41,7 +40,6 @@ import { } from '../../common'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; -import { getFields } from './get_fields'; import { getMappings } from './get_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -49,12 +47,7 @@ import { FindActionResult } from '../../../../actions/server/types'; import { ActionType } from '../../../../actions/common'; import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter } from '../utils'; -import { - ConfigurationGetFields, - MappingsArgs, - CreateMappingsArgs, - UpdateMappingsArgs, -} from './types'; +import { MappingsArgs, CreateMappingsArgs, UpdateMappingsArgs } from './types'; import { createMappings } from './create_mappings'; import { updateMappings } from './update_mappings'; import { @@ -69,7 +62,6 @@ import { * @ignore */ export interface InternalConfigureSubClient { - getFields(params: ConfigurationGetFields): Promise; getMappings( params: MappingsArgs ): Promise['saved_objects']>; @@ -116,12 +108,9 @@ export const createInternalConfigurationSubClient = ( casesClientInternal: CasesClientInternal ): InternalConfigureSubClient => { const configureSubClient: InternalConfigureSubClient = { - getFields: (params: ConfigurationGetFields) => getFields(params, clientArgs), getMappings: (params: MappingsArgs) => getMappings(params, clientArgs), - createMappings: (params: CreateMappingsArgs) => - createMappings(params, clientArgs, casesClientInternal), - updateMappings: (params: UpdateMappingsArgs) => - updateMappings(params, clientArgs, casesClientInternal), + createMappings: (params: CreateMappingsArgs) => createMappings(params, clientArgs), + updateMappings: (params: UpdateMappingsArgs) => updateMappings(params, clientArgs), }; return Object.freeze(configureSubClient); @@ -194,8 +183,7 @@ async function get( if (connector != null) { try { mappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector.id, - connectorType: connector.type, + connector: transformESConnectorToCaseConnector(connector), }); } catch (e) { error = e.isBoom @@ -303,22 +291,22 @@ async function update( try { const resMappings = await casesClientInternal.configuration.getMappings({ - connectorId: connector != null ? connector.id : configuration.attributes.connector.id, - connectorType: connector != null ? connector.type : configuration.attributes.connector.type, + connector: + connector != null + ? connector + : transformESConnectorToCaseConnector(configuration.attributes.connector), }); mappings = resMappings.length > 0 ? resMappings[0].attributes.mappings : []; if (connector != null) { if (resMappings.length !== 0) { mappings = await casesClientInternal.configuration.updateMappings({ - connectorId: connector.id, - connectorType: connector.type, + connector, mappingId: resMappings[0].id, }); } else { mappings = await casesClientInternal.configuration.createMappings({ - connectorId: connector.id, - connectorType: connector.type, + connector, owner: configuration.attributes.owner, }); } @@ -326,9 +314,9 @@ async function update( } catch (e) { error = e.isBoom ? e.output.payload.message - : `Error connecting to ${ + : `Error creating mapping for ${ connector != null ? connector.name : configuration.attributes.connector.name - } instance`; + }`; } const patch = await caseConfigureService.patch({ @@ -429,14 +417,13 @@ async function create( try { mappings = await casesClientInternal.configuration.createMappings({ - connectorId: configuration.connector.id, - connectorType: configuration.connector.type, + connector: configuration.connector, owner: configuration.owner, }); } catch (e) { error = e.isBoom ? e.output.payload.message - : `Error connecting to ${configuration.connector.name} instance`; + : `Error creating mapping for ${configuration.connector.name}`; } const post = await caseConfigureService.post({ diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index b01f10d7a9e43b..bb4c32ae57071a 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -5,40 +5,33 @@ * 2.0. */ -import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappingsAttributes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; -import { CasesClientArgs, CasesClientInternal } from '..'; +import { CasesClientArgs } from '..'; import { CreateMappingsArgs } from './types'; +import { casesConnectors } from '../../connectors'; export const createMappings = async ( - { connectorType, connectorId, owner }: CreateMappingsArgs, - clientArgs: CasesClientArgs, - casesClientInternal: CasesClientInternal + { connector, owner }: CreateMappingsArgs, + clientArgs: CasesClientArgs ): Promise => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { - if (connectorType === ConnectorTypes.none) { - return []; - } - - const res = await casesClientInternal.configuration.getFields({ - connectorId, - connectorType, - }); + const mappings = casesConnectors.get(connector.type)?.getMapping() ?? []; const theMapping = await connectorMappingsService.post({ unsecuredSavedObjectsClient, attributes: { - mappings: res.defaultMappings, + mappings, owner, }, references: [ { type: ACTION_SAVED_OBJECT_TYPE, name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, - id: connectorId, + id: connector.id, }, ], }); @@ -46,7 +39,7 @@ export const createMappings = async ( return theMapping.attributes.mappings; } catch (error) { throw createCaseError({ - message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.ts b/x-pack/plugins/cases/server/client/configure/get_fields.ts deleted file mode 100644 index 78627cfaca6ed1..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; - -import { GetFieldsResponse } from '../../../common/api'; -import { createDefaultMapping, formatFields } from './utils'; -import { CasesClientArgs } from '..'; - -interface ConfigurationGetFields { - connectorId: string; - connectorType: string; -} - -export const getFields = async ( - { connectorType, connectorId }: ConfigurationGetFields, - clientArgs: CasesClientArgs -): Promise => { - const { actionsClient } = clientArgs; - const results = await actionsClient.execute({ - actionId: connectorId, - params: { - subAction: 'getFields', - subActionParams: {}, - }, - }); - if (results.status === 'error') { - throw Boom.failedDependency(results.serviceMessage); - } - const fields = formatFields(results.data, connectorType); - - return { fields, defaultMappings: createDefaultMapping(fields, connectorType) }; -}; diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index 3489c06b1da5ad..2fa0e8454bacfa 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -6,29 +6,25 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { ConnectorMappings, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappings } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; import { CasesClientArgs } from '..'; import { MappingsArgs } from './types'; export const getMappings = async ( - { connectorType, connectorId }: MappingsArgs, + { connector }: MappingsArgs, clientArgs: CasesClientArgs ): Promise['saved_objects']> => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { - if (connectorType === ConnectorTypes.none) { - return []; - } - const myConnectorMappings = await connectorMappingsService.find({ unsecuredSavedObjectsClient, options: { hasReference: { type: ACTION_SAVED_OBJECT_TYPE, - id: connectorId, + id: connector.id, }, }, }); @@ -36,7 +32,7 @@ export const getMappings = async ( return myConnectorMappings.saved_objects; } catch (error) { throw createCaseError({ - message: `Failed to retrieve mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + message: `Failed to retrieve mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/configure/mock.ts b/x-pack/plugins/cases/server/client/configure/mock.ts deleted file mode 100644 index ad982a5cc12434..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/mock.ts +++ /dev/null @@ -1,657 +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 { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; -import { - JiraGetFieldsResponse, - ResilientGetFieldsResponse, - ServiceNowGetFieldsResponse, -} from './utils.test'; -interface TestMappings { - [key: string]: ConnectorMappingsAttributes[]; -} -export const mappings: TestMappings = { - [ConnectorTypes.jira]: [ - { - source: 'title', - target: 'summary', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, - ], - [`${ConnectorTypes.jira}-alt`]: [ - { - source: 'title', - target: 'title', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, - ], - [ConnectorTypes.resilient]: [ - { - source: 'title', - target: 'name', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, - ], - [ConnectorTypes.serviceNowITSM]: [ - { - source: 'title', - target: 'short_description', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - action_type: 'append', - }, - ], - [ConnectorTypes.serviceNowSIR]: [ - { - source: 'title', - target: 'short_description', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - action_type: 'append', - }, - ], -}; - -const jiraFields: JiraGetFieldsResponse = { - summary: { - required: true, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'string', - }, - name: 'Summary', - }, - issuetype: { - required: true, - allowedValues: [ - { - self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10023', - id: '10023', - description: 'A problem or error.', - iconUrl: - 'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10303&avatarType=issuetype', - name: 'Bug', - subtask: false, - avatarId: 10303, - }, - ], - defaultValue: {}, - schema: { - type: 'issuetype', - }, - name: 'Issue Type', - }, - attachment: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'array', - items: 'attachment', - }, - name: 'Attachment', - }, - duedate: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'date', - }, - name: 'Due date', - }, - description: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'string', - }, - name: 'Description', - }, - project: { - required: true, - allowedValues: [ - { - self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10015', - id: '10015', - key: 'RJ2', - name: 'RJ2', - projectTypeKey: 'business', - simplified: false, - avatarUrls: { - '48x48': - 'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10015&avatarId=10412', - '24x24': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10015&avatarId=10412', - '16x16': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10015&avatarId=10412', - '32x32': - 'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10015&avatarId=10412', - }, - }, - ], - defaultValue: {}, - schema: { - type: 'project', - }, - name: 'Project', - }, - assignee: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'user', - }, - name: 'Assignee', - }, - labels: { - required: false, - allowedValues: [], - defaultValue: {}, - schema: { - type: 'array', - items: 'string', - }, - name: 'Labels', - }, -}; -const resilientFields: ResilientGetFieldsResponse = [ - { input_type: 'text', name: 'addr', read_only: false, text: 'Address' }, - { - input_type: 'boolean', - name: 'alberta_health_risk_assessment', - read_only: false, - text: 'Alberta Health Risk Assessment', - }, - { input_type: 'number', name: 'hard_liability', read_only: true, text: 'Assessed Liability' }, - { input_type: 'text', name: 'city', read_only: false, text: 'City' }, - { input_type: 'select', name: 'country', read_only: false, text: 'Country/Region' }, - { input_type: 'select_owner', name: 'creator_id', read_only: true, text: 'Created By' }, - { input_type: 'select', name: 'crimestatus_id', read_only: false, text: 'Criminal Activity' }, - { input_type: 'boolean', name: 'data_encrypted', read_only: false, text: 'Data Encrypted' }, - { input_type: 'select', name: 'data_format', read_only: false, text: 'Data Format' }, - { input_type: 'datetimepicker', name: 'end_date', read_only: true, text: 'Date Closed' }, - { input_type: 'datetimepicker', name: 'create_date', read_only: true, text: 'Date Created' }, - { - input_type: 'datetimepicker', - name: 'determined_date', - read_only: false, - text: 'Date Determined', - }, - { - input_type: 'datetimepicker', - name: 'discovered_date', - read_only: false, - required: 'always', - text: 'Date Discovered', - }, - { input_type: 'datetimepicker', name: 'start_date', read_only: false, text: 'Date Occurred' }, - { input_type: 'select', name: 'exposure_dept_id', read_only: false, text: 'Department' }, - { input_type: 'textarea', name: 'description', read_only: false, text: 'Description' }, - { input_type: 'boolean', name: 'employee_involved', read_only: false, text: 'Employee Involved' }, - { input_type: 'boolean', name: 'data_contained', read_only: false, text: 'Exposure Resolved' }, - { input_type: 'select', name: 'exposure_type_id', read_only: false, text: 'Exposure Type' }, - { - input_type: 'multiselect', - name: 'gdpr_breach_circumstances', - read_only: false, - text: 'GDPR Breach Circumstances', - }, - { input_type: 'select', name: 'gdpr_breach_type', read_only: false, text: 'GDPR Breach Type' }, - { - input_type: 'textarea', - name: 'gdpr_breach_type_comment', - read_only: false, - text: 'GDPR Breach Type Comment', - }, - { input_type: 'select', name: 'gdpr_consequences', read_only: false, text: 'GDPR Consequences' }, - { - input_type: 'textarea', - name: 'gdpr_consequences_comment', - read_only: false, - text: 'GDPR Consequences Comment', - }, - { - input_type: 'select', - name: 'gdpr_final_assessment', - read_only: false, - text: 'GDPR Final Assessment', - }, - { - input_type: 'textarea', - name: 'gdpr_final_assessment_comment', - read_only: false, - text: 'GDPR Final Assessment Comment', - }, - { - input_type: 'select', - name: 'gdpr_identification', - read_only: false, - text: 'GDPR Identification', - }, - { - input_type: 'textarea', - name: 'gdpr_identification_comment', - read_only: false, - text: 'GDPR Identification Comment', - }, - { - input_type: 'select', - name: 'gdpr_personal_data', - read_only: false, - text: 'GDPR Personal Data', - }, - { - input_type: 'textarea', - name: 'gdpr_personal_data_comment', - read_only: false, - text: 'GDPR Personal Data Comment', - }, - { - input_type: 'boolean', - name: 'gdpr_subsequent_notification', - read_only: false, - text: 'GDPR Subsequent Notification', - }, - { input_type: 'number', name: 'id', read_only: true, text: 'ID' }, - { input_type: 'boolean', name: 'impact_likely', read_only: false, text: 'Impact Likely' }, - { - input_type: 'boolean', - name: 'ny_impact_likely', - read_only: false, - text: 'Impact Likely for New York', - }, - { - input_type: 'boolean', - name: 'or_impact_likely', - read_only: false, - text: 'Impact Likely for Oregon', - }, - { - input_type: 'boolean', - name: 'wa_impact_likely', - read_only: false, - text: 'Impact Likely for Washington', - }, - { input_type: 'boolean', name: 'confirmed', read_only: false, text: 'Incident Disposition' }, - { input_type: 'multiselect', name: 'incident_type_ids', read_only: false, text: 'Incident Type' }, - { - input_type: 'text', - name: 'exposure_individual_name', - read_only: false, - text: 'Individual Name', - }, - { - input_type: 'select', - name: 'harmstatus_id', - read_only: false, - text: 'Is harm/risk/misuse foreseeable?', - }, - { input_type: 'text', name: 'jurisdiction_name', read_only: false, text: 'Jurisdiction' }, - { - input_type: 'datetimepicker', - name: 'inc_last_modified_date', - read_only: true, - text: 'Last Modified', - }, - { - input_type: 'multiselect', - name: 'gdpr_lawful_data_processing_categories', - read_only: false, - text: 'Lawful Data Processing Categories', - }, - { input_type: 'multiselect_members', name: 'members', read_only: false, text: 'Members' }, - { input_type: 'text', name: 'name', read_only: false, required: 'always', text: 'Name' }, - { input_type: 'boolean', name: 'negative_pr_likely', read_only: false, text: 'Negative PR' }, - { input_type: 'datetimepicker', name: 'due_date', read_only: true, text: 'Next Due Date' }, - { - input_type: 'multiselect', - name: 'nist_attack_vectors', - read_only: false, - text: 'NIST Attack Vectors', - }, - { input_type: 'select', name: 'org_handle', read_only: true, text: 'Organization' }, - { input_type: 'select_owner', name: 'owner_id', read_only: false, text: 'Owner' }, - { input_type: 'select', name: 'phase_id', read_only: true, text: 'Phase' }, - { - input_type: 'select', - name: 'pipeda_other_factors', - read_only: false, - text: 'PIPEDA Other Factors', - }, - { - input_type: 'textarea', - name: 'pipeda_other_factors_comment', - read_only: false, - text: 'PIPEDA Other Factors Comment', - }, - { - input_type: 'select', - name: 'pipeda_overall_assessment', - read_only: false, - text: 'PIPEDA Overall Assessment', - }, - { - input_type: 'textarea', - name: 'pipeda_overall_assessment_comment', - read_only: false, - text: 'PIPEDA Overall Assessment Comment', - }, - { - input_type: 'select', - name: 'pipeda_probability_of_misuse', - read_only: false, - text: 'PIPEDA Probability of Misuse', - }, - { - input_type: 'textarea', - name: 'pipeda_probability_of_misuse_comment', - read_only: false, - text: 'PIPEDA Probability of Misuse Comment', - }, - { - input_type: 'select', - name: 'pipeda_sensitivity_of_pi', - read_only: false, - text: 'PIPEDA Sensitivity of PI', - }, - { - input_type: 'textarea', - name: 'pipeda_sensitivity_of_pi_comment', - read_only: false, - text: 'PIPEDA Sensitivity of PI Comment', - }, - { input_type: 'text', name: 'reporter', read_only: false, text: 'Reporting Individual' }, - { - input_type: 'select', - name: 'resolution_id', - read_only: false, - required: 'close', - text: 'Resolution', - }, - { - input_type: 'textarea', - name: 'resolution_summary', - read_only: false, - required: 'close', - text: 'Resolution Summary', - }, - { input_type: 'select', name: 'gdpr_harm_risk', read_only: false, text: 'Risk of Harm' }, - { input_type: 'select', name: 'severity_code', read_only: false, text: 'Severity' }, - { input_type: 'boolean', name: 'inc_training', read_only: true, text: 'Simulation' }, - { input_type: 'multiselect', name: 'data_source_ids', read_only: false, text: 'Source of Data' }, - { input_type: 'select', name: 'state', read_only: false, text: 'State' }, - { input_type: 'select', name: 'plan_status', read_only: false, text: 'Status' }, - { input_type: 'select', name: 'exposure_vendor_id', read_only: false, text: 'Vendor' }, - { - input_type: 'boolean', - name: 'data_compromised', - read_only: false, - text: 'Was personal information or personal data involved?', - }, - { - input_type: 'select', - name: 'workspace', - read_only: false, - required: 'always', - text: 'Workspace', - }, - { input_type: 'text', name: 'zip', read_only: false, text: 'Zip' }, -]; -const serviceNowFields: ServiceNowGetFieldsResponse = [ - { - column_label: 'Approval', - mandatory: 'false', - max_length: '40', - element: 'approval', - }, - { - column_label: 'Close notes', - mandatory: 'false', - max_length: '4000', - element: 'close_notes', - }, - { - column_label: 'Contact type', - mandatory: 'false', - max_length: '40', - element: 'contact_type', - }, - { - column_label: 'Correlation display', - mandatory: 'false', - max_length: '100', - element: 'correlation_display', - }, - { - column_label: 'Correlation ID', - mandatory: 'false', - max_length: '100', - element: 'correlation_id', - }, - { - column_label: 'Description', - mandatory: 'false', - max_length: '4000', - element: 'description', - }, - { - column_label: 'Number', - mandatory: 'false', - max_length: '40', - element: 'number', - }, - { - column_label: 'Short description', - mandatory: 'false', - max_length: '160', - element: 'short_description', - }, - { - column_label: 'Created by', - mandatory: 'false', - max_length: '40', - element: 'sys_created_by', - }, - { - column_label: 'Updated by', - mandatory: 'false', - max_length: '40', - element: 'sys_updated_by', - }, - { - column_label: 'Upon approval', - mandatory: 'false', - max_length: '40', - element: 'upon_approval', - }, - { - column_label: 'Upon reject', - mandatory: 'false', - max_length: '40', - element: 'upon_reject', - }, -]; -interface FormatFieldsTestData { - expected: ConnectorField[]; - fields: JiraGetFieldsResponse | ResilientGetFieldsResponse | ServiceNowGetFieldsResponse; - type: ConnectorTypes; -} -export const formatFieldsTestData: FormatFieldsTestData[] = [ - { - expected: [ - { id: 'summary', name: 'Summary', required: true, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'text' }, - ], - fields: jiraFields, - type: ConnectorTypes.jira, - }, - { - expected: [ - { id: 'addr', name: 'Address', required: false, type: 'text' }, - { id: 'city', name: 'City', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { - id: 'gdpr_breach_type_comment', - name: 'GDPR Breach Type Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_consequences_comment', - name: 'GDPR Consequences Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_final_assessment_comment', - name: 'GDPR Final Assessment Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_identification_comment', - name: 'GDPR Identification Comment', - required: false, - type: 'textarea', - }, - { - id: 'gdpr_personal_data_comment', - name: 'GDPR Personal Data Comment', - required: false, - type: 'textarea', - }, - { id: 'exposure_individual_name', name: 'Individual Name', required: false, type: 'text' }, - { id: 'jurisdiction_name', name: 'Jurisdiction', required: false, type: 'text' }, - { id: 'name', name: 'Name', required: true, type: 'text' }, - { - id: 'pipeda_other_factors_comment', - name: 'PIPEDA Other Factors Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_overall_assessment_comment', - name: 'PIPEDA Overall Assessment Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_probability_of_misuse_comment', - name: 'PIPEDA Probability of Misuse Comment', - required: false, - type: 'textarea', - }, - { - id: 'pipeda_sensitivity_of_pi_comment', - name: 'PIPEDA Sensitivity of PI Comment', - required: false, - type: 'textarea', - }, - { id: 'reporter', name: 'Reporting Individual', required: false, type: 'text' }, - { id: 'resolution_summary', name: 'Resolution Summary', required: false, type: 'textarea' }, - { id: 'zip', name: 'Zip', required: false, type: 'text' }, - ], - fields: resilientFields, - type: ConnectorTypes.resilient, - }, - { - expected: [ - { id: 'approval', name: 'Approval', required: false, type: 'text' }, - { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, - { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, - { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, - { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { id: 'number', name: 'Number', required: false, type: 'text' }, - { id: 'short_description', name: 'Short description', required: false, type: 'text' }, - { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, - { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, - { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, - { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, - ], - fields: serviceNowFields, - type: ConnectorTypes.serviceNowITSM, - }, - { - expected: [ - { id: 'approval', name: 'Approval', required: false, type: 'text' }, - { id: 'close_notes', name: 'Close notes', required: false, type: 'textarea' }, - { id: 'contact_type', name: 'Contact type', required: false, type: 'text' }, - { id: 'correlation_display', name: 'Correlation display', required: false, type: 'text' }, - { id: 'correlation_id', name: 'Correlation ID', required: false, type: 'text' }, - { id: 'description', name: 'Description', required: false, type: 'textarea' }, - { id: 'number', name: 'Number', required: false, type: 'text' }, - { id: 'short_description', name: 'Short description', required: false, type: 'text' }, - { id: 'sys_created_by', name: 'Created by', required: false, type: 'text' }, - { id: 'sys_updated_by', name: 'Updated by', required: false, type: 'text' }, - { id: 'upon_approval', name: 'Upon approval', required: false, type: 'text' }, - { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, - ], - fields: serviceNowFields, - type: ConnectorTypes.serviceNowSIR, - }, -]; -export const mockGetFieldsResponse = { - status: 'ok', - data: jiraFields, - actionId: '123', -}; - -export const actionsErrResponse = { - status: 'error', - serviceMessage: 'this is an actions error', -}; diff --git a/x-pack/plugins/cases/server/client/configure/types.ts b/x-pack/plugins/cases/server/client/configure/types.ts index a34251690db48d..aca3436c59082e 100644 --- a/x-pack/plugins/cases/server/client/configure/types.ts +++ b/x-pack/plugins/cases/server/client/configure/types.ts @@ -5,9 +5,10 @@ * 2.0. */ +import { CaseConnector } from '../../../common'; + export interface MappingsArgs { - connectorType: string; - connectorId: string; + connector: CaseConnector; } export interface CreateMappingsArgs extends MappingsArgs { @@ -17,8 +18,3 @@ export interface CreateMappingsArgs extends MappingsArgs { export interface UpdateMappingsArgs extends MappingsArgs { mappingId: string; } - -export interface ConfigurationGetFields { - connectorId: string; - connectorType: string; -} diff --git a/x-pack/plugins/cases/server/client/configure/update_mappings.ts b/x-pack/plugins/cases/server/client/configure/update_mappings.ts index 7eccf4cbbe5829..3d529e51e75614 100644 --- a/x-pack/plugins/cases/server/client/configure/update_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/update_mappings.ts @@ -5,40 +5,33 @@ * 2.0. */ -import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappingsAttributes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { createCaseError } from '../../common/error'; -import { CasesClientArgs, CasesClientInternal } from '..'; +import { CasesClientArgs } from '..'; import { UpdateMappingsArgs } from './types'; +import { casesConnectors } from '../../connectors'; export const updateMappings = async ( - { connectorType, connectorId, mappingId }: UpdateMappingsArgs, - clientArgs: CasesClientArgs, - casesClientInternal: CasesClientInternal + { connector, mappingId }: UpdateMappingsArgs, + clientArgs: CasesClientArgs ): Promise => { const { unsecuredSavedObjectsClient, connectorMappingsService, logger } = clientArgs; try { - if (connectorType === ConnectorTypes.none) { - return []; - } - - const res = await casesClientInternal.configuration.getFields({ - connectorId, - connectorType, - }); + const mappings = casesConnectors.get(connector.type)?.getMapping() ?? []; const theMapping = await connectorMappingsService.update({ unsecuredSavedObjectsClient, mappingId, attributes: { - mappings: res.defaultMappings, + mappings, }, references: [ { type: ACTION_SAVED_OBJECT_TYPE, name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, - id: connectorId, + id: connector.id, }, ], }); @@ -46,7 +39,7 @@ export const updateMappings = async ( return theMapping.attributes.mappings ?? []; } catch (error) { throw createCaseError({ - message: `Failed to create mapping connector id: ${connectorId} type: ${connectorType}: ${error}`, + message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, error, logger, }); diff --git a/x-pack/plugins/cases/server/client/configure/utils.test.ts b/x-pack/plugins/cases/server/client/configure/utils.test.ts deleted file mode 100644 index 41d62f5a9b91f9..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/utils.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { - JiraGetFieldsResponse, - ResilientGetFieldsResponse, - ServiceNowGetFieldsResponse, -} from '../../../../actions/server/types'; -import { createDefaultMapping, formatFields } from './utils'; -import { mappings, formatFieldsTestData } from './mock'; - -describe('client/configure/utils', () => { - describe('formatFields', () => { - formatFieldsTestData.forEach(({ expected, fields, type }) => { - it(`normalizes ${type} fields to common type ConnectorField`, () => { - const result = formatFields(fields, type); - expect(result).toEqual(expected); - }); - }); - }); - describe('createDefaultMapping', () => { - formatFieldsTestData.forEach(({ expected, fields, type }) => { - it(`normalizes ${type} fields to common type ConnectorField`, () => { - const result = createDefaultMapping(expected, type); - expect(result).toEqual(mappings[type]); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts deleted file mode 100644 index 24efb6ca54b3a3..00000000000000 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; -import { - JiraGetFieldsResponse, - ResilientGetFieldsResponse, - ServiceNowGetFieldsResponse, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../actions/server/types'; - -const normalizeJiraFields = (jiraFields: JiraGetFieldsResponse): ConnectorField[] => - Object.keys(jiraFields).reduce( - (acc, data) => - jiraFields[data].schema.type === 'string' - ? [ - ...acc, - { - id: data, - name: jiraFields[data].name, - required: jiraFields[data].required, - type: 'text', - }, - ] - : acc, - [] - ); - -const normalizeResilientFields = (resilientFields: ResilientGetFieldsResponse): ConnectorField[] => - resilientFields.reduce( - (acc: ConnectorField[], data) => - (data.input_type === 'textarea' || data.input_type === 'text') && !data.read_only - ? [ - ...acc, - { - id: data.name, - name: data.text, - required: data.required === 'always', - type: data.input_type, - }, - ] - : acc, - [] - ); -const normalizeServiceNowFields = (snFields: ServiceNowGetFieldsResponse): ConnectorField[] => - snFields.reduce( - (acc, data) => [ - ...acc, - { - id: data.element, - name: data.column_label, - required: data.mandatory === 'true', - type: parseFloat(data.max_length) > 160 ? 'textarea' : 'text', - }, - ], - [] - ); - -export const formatFields = (theData: unknown, theType: string): ConnectorField[] => { - switch (theType) { - case ConnectorTypes.jira: - return normalizeJiraFields(theData as JiraGetFieldsResponse); - case ConnectorTypes.resilient: - return normalizeResilientFields(theData as ResilientGetFieldsResponse); - case ConnectorTypes.serviceNowITSM: - return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); - case ConnectorTypes.serviceNowSIR: - return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); - default: - return []; - } -}; - -const getPreferredFields = (theType: string) => { - let title: string = ''; - let description: string = ''; - let comments: string = ''; - - if (theType === ConnectorTypes.jira) { - title = 'summary'; - description = 'description'; - comments = 'comments'; - } else if (theType === ConnectorTypes.resilient) { - title = 'name'; - description = 'description'; - comments = 'comments'; - } else if ( - theType === ConnectorTypes.serviceNowITSM || - theType === ConnectorTypes.serviceNowSIR - ) { - title = 'short_description'; - description = 'description'; - comments = 'work_notes'; - } - - return { title, description, comments }; -}; - -export const createDefaultMapping = ( - fields: ConnectorField[], - theType: string -): ConnectorMappingsAttributes[] => { - const { description, title, comments } = getPreferredFields(theType); - return [ - { - source: 'title', - target: title, - action_type: 'overwrite', - }, - { - source: 'description', - target: description, - action_type: 'overwrite', - }, - { - source: 'comments', - target: comments, - action_type: 'append', - }, - ]; -}; diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts new file mode 100644 index 00000000000000..64e3e6f3eb225b --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorTypes } from '../../common/api'; +import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; +import { ICasesConnector, CasesConnectorsMap } from './types'; + +const mapping: Record = { + [ConnectorTypes.jira]: getJiraCaseConnector(), + [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), + [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), + [ConnectorTypes.resilient]: getResilientCaseConnector(), + [ConnectorTypes.none]: null, +}; + +const isConnectorTypeSupported = (type: string): type is ConnectorTypes => + Object.values(ConnectorTypes).includes(type as ConnectorTypes); + +export const casesConnectors: CasesConnectorsMap = { + get: (type: string): ICasesConnector | undefined | null => + isConnectorTypeSupported(type) ? mapping[type] : undefined, +}; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index 4b6f845a961f21..b5dc1cc4a8ff9e 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -7,20 +7,16 @@ import { RegisterConnectorsArgs, - ExternalServiceFormatterMapper, CommentSchemaType, ContextTypeGeneratedAlertType, ContextTypeAlertSchemaType, } from './types'; import { getActionType as getCaseConnector } from './case'; -import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_formatter'; -import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter'; -import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; -import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; -import { CommentRequest, CommentType } from '../../common'; +import { CommentRequest, CommentType } from '../../common/api'; export * from './types'; export { transformConnectorComment } from './case'; +export { casesConnectors } from './factory'; /** * Separator used for creating a json parsable array from the mustache syntax that the alerting framework @@ -41,13 +37,6 @@ export const registerConnectors = ({ ); }; -export const externalServiceFormatters: ExternalServiceFormatterMapper = { - '.servicenow': serviceNowITSMExternalServiceFormatter, - '.servicenow-sir': serviceNowSIRExternalServiceFormatter, - '.jira': jiraExternalServiceFormatter, - '.resilient': resilientExternalServiceFormatter, -}; - export const isCommentGeneratedAlert = ( comment: CommentSchemaType | CommentRequest ): comment is ContextTypeGeneratedAlertType => { diff --git a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts b/x-pack/plugins/cases/server/connectors/jira/format.test.ts similarity index 71% rename from x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts rename to x-pack/plugins/cases/server/connectors/jira/format.test.ts index f5d76aeddf3130..edca4cf68250ce 100644 --- a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/jira/format.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; -import { jiraExternalServiceFormatter } from './external_service_formatter'; +import { CaseResponse } from '../../../common/api'; +import { format } from './format'; describe('Jira formatter', () => { const theCase = { @@ -15,21 +15,18 @@ describe('Jira formatter', () => { } as CaseResponse; it('it formats correctly', async () => { - const res = await jiraExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ ...theCase.connector.fields, labels: theCase.tags }); }); it('it formats correctly when fields do not exist ', async () => { const invalidFields = { tags: ['tag'], connector: { fields: null } } as CaseResponse; - const res = await jiraExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ priority: null, issueType: null, parent: null, labels: theCase.tags }); }); it('it replace white spaces with hyphens on tags', async () => { - const res = await jiraExternalServiceFormatter.format( - { ...theCase, tags: ['a tag with spaces'] }, - [] - ); + const res = await format({ ...theCase, tags: ['a tag with spaces'] }, []); expect(res).toEqual({ ...theCase.connector.fields, labels: ['a-tag-with-spaces'] }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/jira/format.ts similarity index 59% rename from x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts rename to x-pack/plugins/cases/server/connectors/jira/format.ts index 15ee2fd468ddaa..4caa23634d8875 100644 --- a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/jira/format.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; +import { ConnectorJiraTypeFields } from '../../../common/api'; +import { Format } from './types'; -interface ExternalServiceParams extends JiraFieldsType { - labels: string[]; -} - -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: Format = (theCase, alerts) => { const { priority = null, issueType = null, parent = null } = (theCase.connector.fields as ConnectorJiraTypeFields['fields']) ?? {}; return { @@ -23,7 +19,3 @@ const format: ExternalServiceFormatter['format'] = (theCa parent, }; }; - -export const jiraExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts b/x-pack/plugins/cases/server/connectors/jira/index.ts similarity index 51% rename from x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts rename to x-pack/plugins/cases/server/connectors/jira/index.ts index 9104d4e7c0b453..9a2a00ac23b390 100644 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts +++ b/x-pack/plugins/cases/server/connectors/jira/index.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; +import { getMapping } from './mapping'; +import { format } from './format'; +import { JiraCaseConnector } from './types'; -export const EXPORT_FAILURE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.components.genericDownloader.exportFailureTitle', - { - defaultMessage: 'Failed to export data…', - } -); +export const getCaseConnector = (): JiraCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/jira/mapping.ts b/x-pack/plugins/cases/server/connectors/jira/mapping.ts new file mode 100644 index 00000000000000..8f8a914b4e091a --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/jira/mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'summary', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/jira/types.ts b/x-pack/plugins/cases/server/connectors/jira/types.ts new file mode 100644 index 00000000000000..59d5741d381b96 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/jira/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JiraFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +interface ExternalServiceFormatterParams extends JiraFieldsType { + labels: string[]; +} + +export type JiraCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts b/x-pack/plugins/cases/server/connectors/resilient/format.test.ts similarity index 76% rename from x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts rename to x-pack/plugins/cases/server/connectors/resilient/format.test.ts index b7096179b0fab6..20ba0bc3789343 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/format.test.ts @@ -6,7 +6,7 @@ */ import { CaseResponse } from '../../../common'; -import { resilientExternalServiceFormatter } from './external_service_formatter'; +import { format } from './format'; describe('IBM Resilient formatter', () => { const theCase = { @@ -14,13 +14,13 @@ describe('IBM Resilient formatter', () => { } as CaseResponse; it('it formats correctly', async () => { - const res = await resilientExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ ...theCase.connector.fields }); }); it('it formats correctly when fields do not exist ', async () => { const invalidFields = { tags: ['a tag'], connector: { fields: null } } as CaseResponse; - const res = await resilientExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ incidentTypes: null, severityCode: null }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/resilient/format.ts similarity index 56% rename from x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts rename to x-pack/plugins/cases/server/connectors/resilient/format.ts index 6dea452565d7c2..3e966d87686d20 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/format.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; +import { ConnectorResillientTypeFields } from '../../../common/api'; +import { Format } from './types'; -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: Format = (theCase, alerts) => { const { incidentTypes = null, severityCode = null } = (theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {}; return { incidentTypes, severityCode }; }; - -export const resilientExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/resilient/index.ts b/x-pack/plugins/cases/server/connectors/resilient/index.ts new file mode 100644 index 00000000000000..a946d0d7fa1c58 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/resilient/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 { getMapping } from './mapping'; +import { format } from './format'; +import { ResilientCaseConnector } from './types'; + +export const getCaseConnector = (): ResilientCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/resilient/mapping.ts b/x-pack/plugins/cases/server/connectors/resilient/mapping.ts new file mode 100644 index 00000000000000..0226073711dfb9 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/resilient/mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'name', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/resilient/types.ts b/x-pack/plugins/cases/server/connectors/resilient/types.ts new file mode 100644 index 00000000000000..f895dccf652145 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/resilient/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ResilientFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export type ResilientCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/index.ts b/x-pack/plugins/cases/server/connectors/servicenow/index.ts new file mode 100644 index 00000000000000..e16a76ff5f79fb --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMapping as getServiceNowITSMMapping } from './itsm_mapping'; +import { format as formatServiceNowITSM } from './itsm_format'; +import { getMapping as getServiceNowSIRMapping } from './sir_mapping'; +import { format as formatServiceNowSIR } from './sir_format'; + +import { ServiceNowITSMCasesConnector, ServiceNowSIRCasesConnector } from './types'; + +export const getServiceNowITSMCaseConnector = (): ServiceNowITSMCasesConnector => ({ + getMapping: getServiceNowITSMMapping, + format: formatServiceNowITSM, +}); + +export const getServiceNowSIRCaseConnector = (): ServiceNowSIRCasesConnector => ({ + getMapping: getServiceNowSIRMapping, + format: formatServiceNowSIR, +}); diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts similarity index 74% rename from x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts rename to x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts index 78242e4c3848ab..2cc1816e7fa67e 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; -import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter'; +import { CaseResponse } from '../../../common/api'; +import { format } from './itsm_format'; describe('ITSM formatter', () => { const theCase = { @@ -16,13 +16,13 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly', async () => { - const res = await serviceNowITSMExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual(theCase.connector.fields); }); it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ severity: null, urgency: null, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts similarity index 58% rename from x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts rename to x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts index a4fa8a198fea77..9bf8c3e7e8b2e0 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; +import { ConnectorServiceNowITSMTypeFields } from '../../../common/api'; +import { ServiceNowITSMFormat } from './types'; -const format: ExternalServiceFormatter['format'] = (theCase) => { +export const format: ServiceNowITSMFormat = (theCase, alerts) => { const { severity = null, urgency = null, impact = null, category = null, subcategory = null } = (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {}; return { severity, urgency, impact, category, subcategory }; }; - -export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.ts new file mode 100644 index 00000000000000..a94d72576d6e38 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceNowITSMGetMapping } from './types'; + +export const getMapping: ServiceNowITSMGetMapping = () => { + return [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts similarity index 90% rename from x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts rename to x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index 1f7716424cfa9b..fa103d4c1142d6 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -6,7 +6,7 @@ */ import { CaseResponse } from '../../../common'; -import { serviceNowSIRExternalServiceFormatter } from './sir_formatter'; +import { format } from './sir_format'; describe('ITSM formatter', () => { const theCase = { @@ -24,7 +24,7 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly without alerts', async () => { - const res = await serviceNowSIRExternalServiceFormatter.format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -38,7 +38,7 @@ describe('ITSM formatter', () => { it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = await serviceNowSIRExternalServiceFormatter.format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -73,7 +73,7 @@ describe('ITSM formatter', () => { url: { full: 'https://attack.com/api' }, }, ]; - const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1,192.168.1.4', source_ip: '192.168.1.2,192.168.1.3', @@ -109,7 +109,7 @@ describe('ITSM formatter', () => { url: { full: 'https://attack.com/api' }, }, ]; - const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1', source_ip: '192.168.1.2,192.168.1.3', @@ -150,7 +150,7 @@ describe('ITSM formatter', () => { connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, } as CaseResponse; - const res = await serviceNowSIRExternalServiceFormatter.format(newCase, alerts); + const res = await format(newCase, alerts); expect(res).toEqual({ dest_ip: null, source_ip: '192.168.1.2,192.168.1.3', diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts similarity index 76% rename from x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts rename to x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 1c528cd2b47bfb..1c6e993d395696 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -5,23 +5,10 @@ * 2.0. */ import { get } from 'lodash/fp'; -import { ConnectorServiceNowSIRTypeFields } from '../../../common'; -import { ExternalServiceFormatter } from '../types'; -interface ExternalServiceParams { - dest_ip: string | null; - source_ip: string | null; - category: string | null; - subcategory: string | null; - malware_hash: string | null; - malware_url: string | null; - priority: string | null; -} -type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url'; -type AlertFieldMappingAndValues = Record< - string, - { alertPath: string; sirFieldKey: SirFieldKey; add: boolean } ->; -const format: ExternalServiceFormatter['format'] = (theCase, alerts) => { +import { ConnectorServiceNowSIRTypeFields } from '../../../common/api'; +import { ServiceNowSIRFormat, SirFieldKey, AlertFieldMappingAndValues } from './types'; + +export const format: ServiceNowSIRFormat = (theCase, alerts) => { const { destIp = null, sourceIp = null, @@ -83,6 +70,3 @@ const format: ExternalServiceFormatter['format'] = (theCa priority, }; }; -export const serviceNowSIRExternalServiceFormatter: ExternalServiceFormatter = { - format, -}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.ts new file mode 100644 index 00000000000000..04d9809bc8b996 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceNowSIRGetMapping } from './types'; + +export const getMapping: ServiceNowSIRGetMapping = () => { + return [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/types.ts b/x-pack/plugins/cases/server/connectors/servicenow/types.ts new file mode 100644 index 00000000000000..500d1d22e3dcb8 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/servicenow/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ServiceNowITSMFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export interface ServiceNowSIRFieldsType { + dest_ip: string | null; + source_ip: string | null; + category: string | null; + subcategory: string | null; + malware_hash: string | null; + malware_url: string | null; + priority: string | null; +} + +export type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url'; +export type AlertFieldMappingAndValues = Record< + string, + { alertPath: string; sirFieldKey: SirFieldKey; add: boolean } +>; + +// ServiceNow ITSM +export type ServiceNowITSMCasesConnector = ICasesConnector; +export type ServiceNowITSMFormat = ICasesConnector['format']; +export type ServiceNowITSMGetMapping = ICasesConnector['getMapping']; + +// ServiceNow SIR +export type ServiceNowSIRCasesConnector = ICasesConnector; +export type ServiceNowSIRFormat = ICasesConnector['format']; +export type ServiceNowSIRGetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/cases/server/connectors/types.ts b/x-pack/plugins/cases/server/connectors/types.ts index 98cbe9683546b6..2fab59037b1bdd 100644 --- a/x-pack/plugins/cases/server/connectors/types.ts +++ b/x-pack/plugins/cases/server/connectors/types.ts @@ -6,7 +6,7 @@ */ import { Logger } from 'kibana/server'; -import { CaseResponse, ConnectorTypes } from '../../common/api'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../common/api'; import { CasesClientGetAlertsResponse } from '../client/alerts/types'; import { CasesClientFactory } from '../client/factory'; import { RegisterActionType } from '../types'; @@ -26,12 +26,11 @@ export interface RegisterConnectorsArgs extends GetActionTypeParams { registerActionType: RegisterActionType; } -export type FormatterConnectorTypes = Exclude; - -export interface ExternalServiceFormatter { +export interface ICasesConnector { format: (theCase: CaseResponse, alerts: CasesClientGetAlertsResponse) => TExternalServiceParams; + getMapping: () => ConnectorMappingsAttributes[]; } -export type ExternalServiceFormatterMapper = { - [x in FormatterConnectorTypes]: ExternalServiceFormatter; -}; +export interface CasesConnectorsMap { + get: (type: string) => ICasesConnector | undefined | null; +} diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index bddceef8d782e6..ef5e0ebb5bbc10 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -10,20 +10,13 @@ import { AssociationType, CaseStatuses, CaseType, - CaseUserActionAttributes, CommentAttributes, CommentType, - ConnectorMappings, ConnectorTypes, ESCaseAttributes, ESCasesConfigureAttributes, } from '../../../../common'; -import { - CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - CASE_USER_ACTION_SAVED_OBJECT, - SECURITY_SOLUTION_OWNER, -} from '../../../../common/constants'; -import { mappings } from '../../../client/configure/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants'; export const mockCases: Array> = [ { @@ -485,79 +478,3 @@ export const mockCaseConfigure: Array> = version: 'WzYsMV0=', }, ]; - -export const mockCaseMappings: Array> = [ - { - type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - id: 'mock-mappings-1', - attributes: { - mappings: mappings[ConnectorTypes.jira], - owner: SECURITY_SOLUTION_OWNER, - }, - references: [], - }, -]; - -export const mockCaseMappingsResilient: Array> = [ - { - type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - id: 'mock-mappings-1', - attributes: { - mappings: mappings[ConnectorTypes.resilient], - owner: SECURITY_SOLUTION_OWNER, - }, - references: [], - }, -]; - -export const mockCaseMappingsBad: Array>> = [ - { - type: CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - id: 'mock-mappings-bad', - attributes: {}, - references: [], - }, -]; - -export const mockUserActions: Array> = [ - { - type: CASE_USER_ACTION_SAVED_OBJECT, - id: 'mock-user-actions-1', - attributes: { - action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], - action: 'create', - action_at: '2021-02-03T17:41:03.771Z', - action_by: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - new_value: - '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', - old_value: null, - owner: SECURITY_SOLUTION_OWNER, - }, - version: 'WzYsMV0=', - references: [], - }, - { - type: CASE_USER_ACTION_SAVED_OBJECT, - id: 'mock-user-actions-2', - attributes: { - action_field: ['comment'], - action: 'create', - action_at: '2021-02-03T17:44:21.067Z', - action_by: { - email: 'elastic@elastic.co', - full_name: 'Elastic', - username: 'elastic', - }, - new_value: - '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', - old_value: null, - owner: SECURITY_SOLUTION_OWNER, - }, - version: 'WzYsMV0=', - references: [], - }, -]; diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index 3934f0ee3417f7..b024a52e647218 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -19,6 +19,7 @@ "lens" ], "requiredBundles": [ + "home", "kibanaReact", "maps", "esUiShared" diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index 20d2e93fd68790..66109de1b14636 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -19,7 +19,7 @@ import type { SecurityPluginSetup } from '../../security/public'; import type { LensPublicStart } from '../../lens/public'; import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api'; import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; -import { registerHomeAddData } from './register_home'; +import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; export interface DataVisualizerSetupDependencies { home?: HomePublicPluginSetup; @@ -48,6 +48,7 @@ export class DataVisualizerPlugin public setup(core: CoreSetup, plugins: DataVisualizerSetupDependencies) { if (plugins.home) { registerHomeAddData(plugins.home); + registerHomeFeatureCatalogue(plugins.home); } } diff --git a/x-pack/plugins/data_visualizer/public/register_home.ts b/x-pack/plugins/data_visualizer/public/register_home.ts index 0b438dc309c4b1..3e8973784433ce 100644 --- a/x-pack/plugins/data_visualizer/public/register_home.ts +++ b/x-pack/plugins/data_visualizer/public/register_home.ts @@ -7,14 +7,34 @@ import { i18n } from '@kbn/i18n'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { FileDataVisualizerWrapper } from './lazy_load_bundle/component_wrapper'; +const FILE_DATA_VIS_TAB_ID = 'fileDataViz'; + export function registerHomeAddData(home: HomePublicPluginSetup) { home.addData.registerAddDataTab({ - id: 'fileDataViz', + id: FILE_DATA_VIS_TAB_ID, name: i18n.translate('xpack.dataVisualizer.file.embeddedTabTitle', { defaultMessage: 'Upload file', }), component: FileDataVisualizerWrapper, }); } + +export function registerHomeFeatureCatalogue(home: HomePublicPluginSetup) { + home.featureCatalogue.register({ + id: `file_data_visualizer`, + title: i18n.translate('xpack.dataVisualizer.title', { + defaultMessage: 'Upload a file', + }), + description: i18n.translate('xpack.dataVisualizer.description', { + defaultMessage: 'Import your own CSV, NDJSON, or log file.', + }), + icon: 'document', + path: `/app/home#/tutorial_directory/${FILE_DATA_VIS_TAB_ID}`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + order: 520, + }); +} diff --git a/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap b/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap new file mode 100644 index 00000000000000..d49743fa487f49 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/__snapshots__/package_to_package_policy.test.ts.snap @@ -0,0 +1,804 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Fleet - packageToPackagePolicy packageToPackagePolicy returns package policy with multiple policy templates (aka has integrations 1`] = ` +Object { + "description": undefined, + "enabled": true, + "inputs": Array [ + Object { + "enabled": true, + "policy_template": "billing", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.billing", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "cost_explorer_config.group_by_dimension_keys": Object { + "type": "text", + "value": Array [ + "AZ", + "INSTANCE_TYPE", + "SERVICE", + ], + }, + "cost_explorer_config.group_by_tag_keys": Object { + "type": "text", + "value": Array [ + "aws:createdBy", + ], + }, + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "12h", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "cloudtrail", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.cloudtrail", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "api_timeout": Object { + "type": "text", + "value": undefined, + }, + "fips_enabled": Object { + "type": "bool", + "value": false, + }, + "queue_url": Object { + "type": "text", + "value": undefined, + }, + "visibility_timeout": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "s3", + }, + Object { + "enabled": false, + "policy_template": "cloudtrail", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.cloudtrail", + "type": "logs", + }, + "enabled": false, + "vars": Object { + "interval": Object { + "type": "text", + "value": "10s", + }, + "password": Object { + "type": "password", + "value": undefined, + }, + "search": Object { + "type": "text", + "value": "search sourcetype=aws:cloudtrail", + }, + "ssl": Object { + "type": "yaml", + "value": undefined, + }, + "tags": Object { + "type": "text", + "value": Array [ + "forwarded", + ], + }, + "url": Object { + "type": "text", + "value": "https://server.example.com:8089", + }, + "username": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "httpjson", + }, + Object { + "enabled": true, + "policy_template": "cloudwatch", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.cloudwatch_logs", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "api_timeout": Object { + "type": "text", + "value": undefined, + }, + "fips_enabled": Object { + "type": "bool", + "value": false, + }, + "queue_url": Object { + "type": "text", + "value": undefined, + }, + "visibility_timeout": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "s3", + }, + Object { + "enabled": true, + "policy_template": "cloudwatch", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.cloudwatch_metrics", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "metrics": Object { + "type": "yaml", + "value": "- namespace: AWS/EC2 + resource_type: ec2:instance + name: + - CPUUtilization + - DiskWriteOps + statistic: + - Average + - Maximum + # dimensions: + # - name: InstanceId + # value: i-123456 + # tags: + # - key: created-by + # value: foo +", + }, + "period": Object { + "type": "text", + "value": "300s", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "dynamodb", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.dynamodb", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "5m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "ebs", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.ebs", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "5m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "ec2", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.ec2_logs", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "api_timeout": Object { + "type": "text", + "value": undefined, + }, + "fips_enabled": Object { + "type": "bool", + "value": false, + }, + "queue_url": Object { + "type": "text", + "value": undefined, + }, + "visibility_timeout": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "s3", + }, + Object { + "enabled": true, + "policy_template": "ec2", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.ec2_metrics", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "5m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "elb", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.elb_logs", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "api_timeout": Object { + "type": "text", + "value": undefined, + }, + "fips_enabled": Object { + "type": "bool", + "value": false, + }, + "queue_url": Object { + "type": "text", + "value": undefined, + }, + "visibility_timeout": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "s3", + }, + Object { + "enabled": true, + "policy_template": "elb", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.elb_metrics", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "lambda", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.lambda", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "5m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "natgateway", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.natgateway", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "rds", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.rds", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "s3", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.s3access", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "api_timeout": Object { + "type": "text", + "value": undefined, + }, + "fips_enabled": Object { + "type": "bool", + "value": false, + }, + "queue_url": Object { + "type": "text", + "value": undefined, + }, + "visibility_timeout": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "s3", + }, + Object { + "enabled": true, + "policy_template": "s3", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.s3_daily_storage", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "24h", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + Object { + "data_stream": Object { + "dataset": "aws.s3_request", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "sns", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.sns", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "5m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "sqs", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.sqs", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "5m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "transitgateway", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.transitgateway", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "usage", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.usage", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + }, + }, + ], + "type": "aws/metrics", + }, + Object { + "enabled": true, + "policy_template": "vpcflow", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.vpcflow", + "type": "logs", + }, + "enabled": true, + "vars": Object { + "api_timeout": Object { + "type": "text", + "value": undefined, + }, + "fips_enabled": Object { + "type": "bool", + "value": false, + }, + "queue_url": Object { + "type": "text", + "value": undefined, + }, + "visibility_timeout": Object { + "type": "text", + "value": undefined, + }, + }, + }, + ], + "type": "s3", + }, + Object { + "enabled": true, + "policy_template": "vpn", + "streams": Array [ + Object { + "data_stream": Object { + "dataset": "aws.vpn", + "type": "metrics", + }, + "enabled": true, + "vars": Object { + "latency": Object { + "type": "text", + "value": undefined, + }, + "period": Object { + "type": "text", + "value": "1m", + }, + "regions": Object { + "type": "text", + "value": Array [], + }, + "tags_filter": Object { + "type": "yaml", + "value": "# - key: \\"created-by\\" + # value: \\"foo\\" +", + }, + }, + }, + ], + "type": "aws/metrics", + }, + ], + "name": "aws-1", + "namespace": "default", + "output_id": "some-output-id", + "package": Object { + "name": "aws", + "title": "AWS", + "version": "0.5.3", + }, + "policy_id": "some-agent-policy-id", + "vars": Object { + "access_key_id": Object { + "type": "text", + "value": undefined, + }, + "credential_profile_name": Object { + "type": "text", + "value": undefined, + }, + "endpoint": Object { + "type": "text", + "value": "amazonaws.com", + }, + "role_arn": Object { + "type": "text", + "value": undefined, + }, + "secret_access_key": Object { + "type": "text", + "value": undefined, + }, + "session_token": Object { + "type": "text", + "value": undefined, + }, + "shared_credential_file": Object { + "type": "text", + "value": undefined, + }, + }, +} +`; diff --git a/x-pack/plugins/fleet/common/services/__snapshots__/validate_package_policy.test.ts.snap b/x-pack/plugins/fleet/common/services/__snapshots__/validate_package_policy.test.ts.snap new file mode 100644 index 00000000000000..72f4182d87629f --- /dev/null +++ b/x-pack/plugins/fleet/common/services/__snapshots__/validate_package_policy.test.ts.snap @@ -0,0 +1,301 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Fleet - validatePackagePolicy() works for packages with multiple policy templates (aka integrations) returns errors for invalid package policy 1`] = ` +Object { + "description": null, + "inputs": Object { + "billing-aws/metrics": Object { + "streams": Object { + "aws.billing": Object { + "vars": Object { + "cost_explorer_config.group_by_dimension_keys": null, + "cost_explorer_config.group_by_tag_keys": null, + "latency": null, + "period": null, + }, + }, + }, + }, + "cloudtrail-httpjson": Object { + "streams": Object { + "aws.cloudtrail": Object { + "vars": Object { + "interval": null, + "password": null, + "search": null, + "ssl": null, + "tags": null, + "url": null, + "username": null, + }, + }, + }, + }, + "cloudtrail-s3": Object { + "streams": Object { + "aws.cloudtrail": Object { + "vars": Object { + "api_timeout": null, + "fips_enabled": null, + "queue_url": Array [ + "Queue URL is required", + ], + "visibility_timeout": null, + }, + }, + }, + }, + "cloudwatch-aws/metrics": Object { + "streams": Object { + "aws.cloudwatch_metrics": Object { + "vars": Object { + "latency": null, + "metrics": null, + "period": null, + "regions": null, + }, + }, + }, + }, + "cloudwatch-s3": Object { + "streams": Object { + "aws.cloudwatch_logs": Object { + "vars": Object { + "api_timeout": null, + "fips_enabled": null, + "queue_url": Array [ + "Queue URL is required", + ], + "visibility_timeout": null, + }, + }, + }, + }, + "dynamodb-aws/metrics": Object { + "streams": Object { + "aws.dynamodb": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "ebs-aws/metrics": Object { + "streams": Object { + "aws.ebs": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "ec2-aws/metrics": Object { + "streams": Object { + "aws.ec2_metrics": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "ec2-s3": Object { + "streams": Object { + "aws.ec2_logs": Object { + "vars": Object { + "api_timeout": null, + "fips_enabled": null, + "queue_url": Array [ + "Queue URL is required", + ], + "visibility_timeout": null, + }, + }, + }, + }, + "elb-aws/metrics": Object { + "streams": Object { + "aws.elb_metrics": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "elb-s3": Object { + "streams": Object { + "aws.elb_logs": Object { + "vars": Object { + "api_timeout": null, + "fips_enabled": null, + "queue_url": Array [ + "Queue URL is required", + ], + "visibility_timeout": null, + }, + }, + }, + }, + "lambda-aws/metrics": Object { + "streams": Object { + "aws.lambda": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "natgateway-aws/metrics": Object { + "streams": Object { + "aws.natgateway": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + }, + }, + }, + }, + "rds-aws/metrics": Object { + "streams": Object { + "aws.rds": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "s3-aws/metrics": Object { + "streams": Object { + "aws.s3_daily_storage": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + }, + }, + "aws.s3_request": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + }, + }, + }, + }, + "s3-s3": Object { + "streams": Object { + "aws.s3access": Object { + "vars": Object { + "api_timeout": null, + "fips_enabled": null, + "queue_url": Array [ + "Queue URL is required", + ], + "visibility_timeout": null, + }, + }, + }, + }, + "sns-aws/metrics": Object { + "streams": Object { + "aws.sns": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + "sqs-aws/metrics": Object { + "streams": Object { + "aws.sqs": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + }, + }, + }, + }, + "transitgateway-aws/metrics": Object { + "streams": Object { + "aws.transitgateway": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + }, + }, + }, + }, + "usage-aws/metrics": Object { + "streams": Object { + "aws.usage": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + }, + }, + }, + }, + "vpcflow-s3": Object { + "streams": Object { + "aws.vpcflow": Object { + "vars": Object { + "api_timeout": null, + "fips_enabled": null, + "queue_url": Array [ + "Queue URL is required", + ], + "visibility_timeout": null, + }, + }, + }, + }, + "vpn-aws/metrics": Object { + "streams": Object { + "aws.vpn": Object { + "vars": Object { + "latency": null, + "period": null, + "regions": null, + "tags_filter": null, + }, + }, + }, + }, + }, + "name": null, + "namespace": null, + "vars": Object { + "access_key_id": null, + "credential_profile_name": null, + "endpoint": null, + "role_arn": null, + "secret_access_key": null, + "session_token": null, + "shared_credential_file": null, + }, +} +`; diff --git a/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts b/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts new file mode 100644 index 00000000000000..2b93cca3d4e4d2 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/fixtures/aws_package.ts @@ -0,0 +1,2742 @@ +/* + * 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 AWS_PACKAGE = { + name: 'aws', + title: 'AWS', + version: '0.5.3', + release: 'beta', + description: 'AWS Integration', + type: 'integration', + download: '/epr/aws/aws-0.5.3.zip', + path: '/package/aws/0.5.3', + icons: [ + { + src: '/img/logo_aws.svg', + path: '/package/aws/0.5.3/img/logo_aws.svg', + title: 'logo aws', + size: '32x32', + type: 'image/svg+xml', + }, + ], + format_version: '1.0.0', + readme: '/package/aws/0.5.3/docs/README.md', + license: 'basic', + categories: ['aws', 'cloud', 'network', 'security'], + conditions: { + 'kibana.version': '^7.12.0', + }, + screenshots: [ + { + src: '/img/metricbeat-aws-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-overview.png', + title: 'metricbeat aws overview', + size: '3848x2440', + type: 'image/png', + }, + ], + assets: { + kibana: { + dashboard: [], + map: [], + search: [], + visualization: [], + }, + elasticsearch: { + ingest_pipeline: [], + }, + }, + policy_templates: [ + { + name: 'billing', + title: 'AWS Billing', + description: 'Collect AWS billing metrics', + data_streams: ['billing'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect billing metrics', + description: 'Collect billing metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_billing.svg', + path: '/package/aws/0.5.3/img/logo_billing.svg', + title: 'AWS Billing logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/metricbeat-aws-billing-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-billing-overview.png', + title: 'metricbeat aws billing overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/billing.md', + }, + { + name: 'cloudtrail', + title: 'AWS Cloudtrail', + description: 'Collect logs from AWS Cloudtrail', + data_streams: ['cloudtrail'], + inputs: [ + { + type: 's3', + title: 'Collect logs from Cloudtrail service', + description: 'Collecting Cloudtrail logs using S3 input', + input_group: 'logs', + }, + { + type: 'httpjson', + title: 'Collect logs from third-party REST API (experimental)', + description: 'Collect logs from third-party REST API (experimental)', + input_group: 'logs', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_cloudtrail.svg', + path: '/package/aws/0.5.3/img/logo_cloudtrail.svg', + title: 'AWS Cloudtrail logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/filebeat-aws-cloudtrail.png', + path: '/package/aws/0.5.3/img/filebeat-aws-cloudtrail.png', + title: 'filebeat aws cloudtrail', + size: '1702x1063', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/cloudtrail.md', + }, + { + name: 'cloudwatch', + title: 'AWS CloudWatch', + description: 'Collect logs and metrics from CloudWatch', + data_streams: ['cloudwatch_logs', 'cloudwatch_metrics'], + inputs: [ + { + type: 's3', + title: 'Collect logs from CloudWatch', + description: 'Collecting logs from CloudWatch using S3 input', + input_group: 'logs', + }, + { + type: 'aws/metrics', + title: 'Collect metrics from CloudWatch', + description: 'Collecting metrics from AWS CloudWatch', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_cloudwatch.svg', + path: '/package/aws/0.5.3/img/logo_cloudwatch.svg', + title: 'AWS CloudWatch logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + readme: '/package/aws/0.5.3/docs/cloudwatch.md', + }, + { + name: 'dynamodb', + title: 'AWS DynamoDB', + description: 'Collect AWS DynamoDB metrics', + data_streams: ['dynamodb'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect dynamodb metrics', + description: 'Collect dynamodb metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_dynamodb.svg', + path: '/package/aws/0.5.3/img/logo_dynamodb.svg', + title: 'AWS DynamoDB logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + categories: ['datastore'], + screenshots: [ + { + src: '/img/metricbeat-aws-dynamodb-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-dynamodb-overview.png', + title: 'metricbeat aws dynamodb overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/dynamodb.md', + }, + { + name: 'ebs', + title: 'AWS EBS', + description: 'Collect AWS EBS metrics', + data_streams: ['ebs'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect EBS metrics', + description: 'Collect EBS metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_ebs.svg', + path: '/package/aws/0.5.3/img/logo_ebs.svg', + title: 'AWS EBS logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + categories: ['datastore'], + screenshots: [ + { + src: '/img/metricbeat-aws-ebs-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-ebs-overview.png', + title: 'metricbeat aws ebs overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/ebs.md', + }, + { + name: 'ec2', + title: 'AWS EC2', + description: 'Collect logs and metrics from EC2 service', + data_streams: ['ec2_logs', 'ec2_metrics'], + inputs: [ + { + type: 's3', + title: 'Collect logs from EC2 service', + description: 'Collecting EC2 logs using S3 input', + input_group: 'logs', + }, + { + type: 'aws/metrics', + title: 'Collect metrics from EC2 service', + description: 'Collecting EC2 metrics using AWS CloudWatch', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_ec2.svg', + path: '/package/aws/0.5.3/img/logo_ec2.svg', + title: 'AWS EC2 logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/metricbeat-aws-ec2-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-ec2-overview.png', + title: 'metricbeat aws ec2 overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/ec2.md', + }, + { + name: 'elb', + title: 'AWS ELB', + description: 'Collect logs and metrics from ELB service', + data_streams: ['elb_logs', 'elb_metrics'], + inputs: [ + { + type: 's3', + title: 'Collect logs from ELB service', + description: 'Collecting ELB logs using S3 input', + input_group: 'logs', + }, + { + type: 'aws/metrics', + title: 'Collect metrics from ELB service', + description: 'Collecting ELB metrics using AWS CloudWatch', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_elb.svg', + path: '/package/aws/0.5.3/img/logo_elb.svg', + title: 'AWS ELB logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/metricbeat-aws-elb-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-elb-overview.png', + title: 'metricbeat aws elb overview', + size: '2640x2240', + type: 'image/png', + }, + { + src: '/img/filebeat-aws-elb-overview.png', + path: '/package/aws/0.5.3/img/filebeat-aws-elb-overview.png', + title: 'filebeat aws elb overview', + size: '1684x897', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/elb.md', + }, + { + name: 'lambda', + title: 'AWS Lambda', + description: 'Collect AWS Lambda metrics', + data_streams: ['lambda'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect Lambda metrics', + description: 'Collect Lambda metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_lambda.svg', + path: '/package/aws/0.5.3/img/logo_lambda.svg', + title: 'AWS Lambda logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/metricbeat-aws-lambda-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-lambda-overview.png', + title: 'metricbeat aws lambda overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/lambda.md', + }, + { + name: 'natgateway', + title: 'AWS NATGateway', + description: 'Collect AWS NATGateway metrics', + data_streams: ['natgateway'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect NATGateway metrics', + description: 'Collect NATGateway metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_natgateway.svg', + path: '/package/aws/0.5.3/img/logo_natgateway.svg', + title: 'AWS NATGateway logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + readme: '/package/aws/0.5.3/docs/natgateway.md', + }, + { + name: 'rds', + title: 'AWS RDS', + description: 'Collect AWS RDS metrics', + data_streams: ['rds'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect RDS metrics', + description: 'Collect RDS metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_rds.svg', + path: '/package/aws/0.5.3/img/logo_rds.svg', + title: 'AWS RDS logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + categories: ['datastore'], + screenshots: [ + { + src: '/img/metricbeat-aws-rds-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-rds-overview.png', + title: 'metricbeat aws rds overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/rds.md', + }, + { + name: 's3', + title: 'AWS S3', + description: 'Collect AWS S3 metrics', + data_streams: ['s3_daily_storage', 's3_request', 's3access'], + inputs: [ + { + type: 's3', + title: 'Collect S3 access logs', + description: 'Collecting S3 access logs using S3 input', + input_group: 'logs', + }, + { + type: 'aws/metrics', + title: 'Collect metrics from S3', + description: 'Collecting S3 metrics using AWS CloudWatch', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_s3.svg', + path: '/package/aws/0.5.3/img/logo_s3.svg', + title: 'AWS S3 logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + categories: ['datastore'], + screenshots: [ + { + src: '/img/metricbeat-aws-s3-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-s3-overview.png', + title: 'metricbeat aws s3 overview', + size: '2640x2240', + type: 'image/png', + }, + { + src: '/img/filebeat-aws-s3access-overview.png', + path: '/package/aws/0.5.3/img/filebeat-aws-s3access-overview.png', + title: 'filebeat aws s3access overview', + size: '1684x897', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/s3.md', + }, + { + name: 'sns', + title: 'AWS SNS', + description: 'Collect AWS SNS metrics', + data_streams: ['sns'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect SNS metrics', + description: 'Collect SNS metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_sns.svg', + path: '/package/aws/0.5.3/img/logo_sns.svg', + title: 'AWS SNS logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/metricbeat-aws-sns-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-sns-overview.png', + title: 'metricbeat aws sns overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/sns.md', + }, + { + name: 'sqs', + title: 'AWS SQS', + description: 'Collect AWS SQS metrics', + data_streams: ['sqs'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect SQS metrics', + description: 'Collect SQS metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_sqs.svg', + path: '/package/aws/0.5.3/img/logo_sqs.svg', + title: 'AWS SQS logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + screenshots: [ + { + src: '/img/metricbeat-aws-sqs-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-sqs-overview.png', + title: 'metricbeat aws sqs overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/sqs.md', + }, + { + name: 'transitgateway', + title: 'AWS Transit Gateway', + description: 'Collect AWS Transit Gateway metrics', + data_streams: ['transitgateway'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect Transit Gateway metrics', + description: 'Collect Transit Gateway metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_transitgateway.svg', + path: '/package/aws/0.5.3/img/logo_transitgateway.svg', + title: 'AWS Transit Gateway logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + readme: '/package/aws/0.5.3/docs/transitgateway.md', + }, + { + name: 'usage', + title: 'AWS Usage', + description: 'Collect AWS Usage metrics', + data_streams: ['usage'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect Usage metrics', + description: 'Collect Usage metrics', + input_group: 'metrics', + }, + ], + multiple: true, + screenshots: [ + { + src: '/img/metricbeat-aws-usage-overview.png', + path: '/package/aws/0.5.3/img/metricbeat-aws-usage-overview.png', + title: 'metricbeat aws sns overview', + size: '2640x2240', + type: 'image/png', + }, + ], + readme: '/package/aws/0.5.3/docs/usage.md', + }, + { + name: 'vpcflow', + title: 'AWS VPC Flow', + description: 'Collect AWS vpcflow logs', + data_streams: ['vpcflow'], + inputs: [ + { + type: 's3', + title: 'Collect VPC Flow logs', + description: 'Collecting VPC Flow logs using S3 input', + input_group: 'logs', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_vpcflow.svg', + path: '/package/aws/0.5.3/img/logo_vpcflow.svg', + title: 'AWS VPC logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + readme: '/package/aws/0.5.3/docs/vpcflow.md', + }, + { + name: 'vpn', + title: 'AWS VPN', + description: 'Collect AWS VPN metrics', + data_streams: ['vpn'], + inputs: [ + { + type: 'aws/metrics', + title: 'Collect VPN metrics', + description: 'Collect VPN metrics', + input_group: 'metrics', + }, + ], + multiple: true, + icons: [ + { + src: '/img/logo_vpn.svg', + path: '/package/aws/0.5.3/img/logo_vpn.svg', + title: 'AWS VPN logo', + size: '32x32', + type: 'image/svg+xml', + }, + ], + categories: ['network'], + readme: '/package/aws/0.5.3/docs/vpn.md', + }, + ], + data_streams: [ + { + type: 'metrics', + dataset: 'aws.billing', + title: 'AWS billing metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '12h', + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'cost_explorer_config.group_by_dimension_keys', + type: 'text', + title: 'Cost Explorer Group By Dimension Keys', + multi: true, + required: false, + show_user: true, + default: ['AZ', 'INSTANCE_TYPE', 'SERVICE'], + }, + { + name: 'cost_explorer_config.group_by_tag_keys', + type: 'text', + title: 'Cost Explorer Group By Tag Keys', + multi: true, + required: false, + show_user: true, + default: ['aws:createdBy'], + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS Billing metrics', + description: 'Collect AWS billing metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'billing', + }, + { + type: 'logs', + dataset: 'aws.cloudtrail', + title: 'AWS CloudTrail logs', + release: 'beta', + ingest_pipeline: 'default', + streams: [ + { + input: 's3', + vars: [ + { + name: 'visibility_timeout', + type: 'text', + title: 'Visibility Timeout', + description: + 'The duration that the received messages are hidden from subsequent retrieve requests after being retrieved by a ReceiveMessage request. The maximum is 12 hours.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'api_timeout', + type: 'text', + title: 'API Timeout', + description: + 'The maximum duration of AWS API can take. The maximum is half of the visibility timeout value.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'queue_url', + type: 'text', + title: 'Queue URL', + description: 'URL of the AWS SQS queue that messages will be received from.', + multi: false, + required: true, + show_user: true, + }, + { + name: 'fips_enabled', + type: 'bool', + title: 'Enable S3 FIPS', + description: + 'Enabling this option changes the service name from `s3` to `s3-fips` for connecting to the correct service endpoint.', + multi: false, + required: false, + show_user: false, + default: false, + }, + ], + template_path: 's3.yml.hbs', + title: 'AWS CloudTrail logs', + description: 'Collect AWS CloudTrail logs using s3 input', + enabled: true, + }, + { + input: 'httpjson', + vars: [ + { + name: 'url', + type: 'text', + title: 'URL of Splunk Enterprise Server', + description: 'i.e. scheme://host:port, path is automatic', + multi: false, + required: true, + show_user: true, + default: 'https://server.example.com:8089', + }, + { + name: 'username', + type: 'text', + title: 'Splunk REST API Username', + multi: false, + required: true, + show_user: true, + }, + { + name: 'password', + type: 'password', + title: 'Splunk REST API Password', + multi: false, + required: true, + show_user: true, + }, + { + name: 'ssl', + type: 'yaml', + title: 'SSL Configuration', + description: + 'i.e. certificate_authorities, supported_protocols, verification_mode etc.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'interval', + type: 'text', + title: 'Interval to query Splunk Enterprise REST API', + description: 'Go Duration syntax (eg. 10s)', + multi: false, + required: true, + show_user: true, + default: '10s', + }, + { + name: 'search', + type: 'text', + title: 'Splunk search string', + multi: false, + required: true, + show_user: true, + default: 'search sourcetype=aws:cloudtrail', + }, + { + name: 'tags', + type: 'text', + title: 'Tags', + multi: true, + required: false, + show_user: false, + default: ['forwarded'], + }, + ], + template_path: 'httpjson.yml.hbs', + title: 'AWS CloudTrail logs via Splunk Enterprise REST API', + description: 'Collect AWS CloudTrail logs via Splunk Enterprise REST API', + enabled: false, + }, + ], + package: 'aws', + path: 'cloudtrail', + }, + { + type: 'logs', + dataset: 'aws.cloudwatch_logs', + title: 'AWS CloudWatch logs', + release: 'beta', + ingest_pipeline: 'default', + streams: [ + { + input: 's3', + vars: [ + { + name: 'visibility_timeout', + type: 'text', + title: 'Visibility Timeout', + description: + 'The duration that the received messages are hidden from subsequent retrieve requests after being retrieved by a ReceiveMessage request. The maximum is 12 hours.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'api_timeout', + type: 'text', + title: 'API Timeout', + description: + 'The maximum duration of AWS API can take. The maximum is half of the visibility timeout value.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'queue_url', + type: 'text', + title: 'Queue URL', + description: 'URL of the AWS SQS queue that messages will be received from.', + multi: false, + required: true, + show_user: true, + }, + { + name: 'fips_enabled', + type: 'bool', + title: 'Enable S3 FIPS', + description: + 'Enabling this option changes the service name from `s3` to `s3-fips` for connecting to the correct service endpoint.', + multi: false, + required: false, + show_user: false, + default: false, + }, + ], + template_path: 's3.yml.hbs', + title: 'AWS CloudWatch logs', + description: 'Collect AWS CloudWatch logs using s3 input', + enabled: true, + }, + ], + package: 'aws', + path: 'cloudwatch_logs', + }, + { + type: 'metrics', + dataset: 'aws.cloudwatch_metrics', + title: 'AWS CloudWatch metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '300s', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'metrics', + type: 'yaml', + title: 'Metrics', + multi: false, + required: true, + show_user: true, + default: + '- namespace: AWS/EC2\n resource_type: ec2:instance\n name:\n - CPUUtilization\n - DiskWriteOps\n statistic:\n - Average\n - Maximum\n # dimensions:\n # - name: InstanceId\n # value: i-123456\n # tags:\n # - key: created-by\n # value: foo\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS CloudWatch metrics', + description: 'Collect AWS CloudWatch metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'cloudwatch_metrics', + }, + { + type: 'metrics', + dataset: 'aws.dynamodb', + title: 'AWS DynamoDB metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '5m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS DynamoDB metrics', + description: 'Collect AWS DynamoDB metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'dynamodb', + }, + { + type: 'metrics', + dataset: 'aws.ebs', + title: 'AWS EBS metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '5m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS EBS metrics', + description: 'Collect AWS EBS metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'ebs', + }, + { + type: 'logs', + dataset: 'aws.ec2_logs', + title: 'AWS EC2 logs', + release: 'beta', + ingest_pipeline: 'default', + streams: [ + { + input: 's3', + vars: [ + { + name: 'visibility_timeout', + type: 'text', + title: 'Visibility Timeout', + description: + 'The duration that the received messages are hidden from subsequent retrieve requests after being retrieved by a ReceiveMessage request. The maximum is 12 hours.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'api_timeout', + type: 'text', + title: 'API Timeout', + description: + 'The maximum duration of AWS API can take. The maximum is half of the visibility timeout value.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'queue_url', + type: 'text', + title: 'Queue URL', + description: 'URL of the AWS SQS queue that messages will be received from.', + multi: false, + required: true, + show_user: true, + }, + { + name: 'fips_enabled', + type: 'bool', + title: 'Enable S3 FIPS', + description: + 'Enabling this option changes the service name from `s3` to `s3-fips` for connecting to the correct service endpoint.', + multi: false, + required: false, + show_user: false, + default: false, + }, + ], + template_path: 's3.yml.hbs', + title: 'AWS EC2 logs', + description: 'Collect AWS EC2 logs using s3 input', + enabled: true, + }, + ], + package: 'aws', + path: 'ec2_logs', + }, + { + type: 'metrics', + dataset: 'aws.ec2_metrics', + title: 'AWS EC2 metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '5m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS EC2 metrics', + description: 'Collect AWS EC2 metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'ec2_metrics', + }, + { + type: 'logs', + dataset: 'aws.elb_logs', + title: 'AWS ELB logs', + release: 'beta', + ingest_pipeline: 'default', + streams: [ + { + input: 's3', + vars: [ + { + name: 'visibility_timeout', + type: 'text', + title: 'Visibility Timeout', + description: + 'The duration that the received messages are hidden from subsequent retrieve requests after being retrieved by a ReceiveMessage request. The maximum is 12 hours.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'api_timeout', + type: 'text', + title: 'API Timeout', + description: + 'The maximum duration of AWS API can take. The maximum is half of the visibility timeout value.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'queue_url', + type: 'text', + title: 'Queue URL', + description: 'URL of the AWS SQS queue that messages will be received from.', + multi: false, + required: true, + show_user: true, + }, + { + name: 'fips_enabled', + type: 'bool', + title: 'Enable S3 FIPS', + description: + 'Enabling this option changes the service name from `s3` to `s3-fips` for connecting to the correct service endpoint.', + multi: false, + required: false, + show_user: false, + default: false, + }, + ], + template_path: 's3.yml.hbs', + title: 'AWS ELB logs', + description: 'Collect AWS ELB logs using s3 input', + enabled: true, + }, + ], + package: 'aws', + path: 'elb_logs', + }, + { + type: 'metrics', + dataset: 'aws.elb_metrics', + title: 'AWS ELB metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS ELB metrics', + description: 'Collect AWS ELB metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'elb_metrics', + }, + { + type: 'metrics', + dataset: 'aws.lambda', + title: 'AWS Lambda metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '5m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS Lambda metrics', + description: 'Collect AWS Lambda metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'lambda', + }, + { + type: 'metrics', + dataset: 'aws.natgateway', + title: 'AWS NAT gateway metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS NAT gateway metrics', + description: 'Collect AWS NAT gateway metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'natgateway', + }, + { + type: 'metrics', + dataset: 'aws.rds', + title: 'AWS RDS metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS RDS metrics', + description: 'Collect AWS RDS metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'rds', + }, + { + type: 'metrics', + dataset: 'aws.s3_daily_storage', + title: 'AWS S3 daily storage metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '24h', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS S3 daily storage metrics', + description: 'Collect AWS S3 daily storage metrics', + enabled: true, + }, + ], + package: 'aws', + path: 's3_daily_storage', + }, + { + type: 'metrics', + dataset: 'aws.s3_request', + title: 'AWS S3 request metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS S3 request metrics', + description: 'Collect AWS S3 request metrics', + enabled: true, + }, + ], + package: 'aws', + path: 's3_request', + }, + { + type: 'logs', + dataset: 'aws.s3access', + title: 'AWS s3access logs', + release: 'beta', + ingest_pipeline: 'default', + streams: [ + { + input: 's3', + vars: [ + { + name: 'visibility_timeout', + type: 'text', + title: 'Visibility Timeout', + description: + 'The duration that the received messages are hidden from subsequent retrieve requests after being retrieved by a ReceiveMessage request. The maximum is 12 hours.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'api_timeout', + type: 'text', + title: 'API Timeout', + description: + 'The maximum duration of AWS API can take. The maximum is half of the visibility timeout value.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'queue_url', + type: 'text', + title: 'Queue URL', + description: 'URL of the AWS SQS queue that messages will be received from.', + multi: false, + required: true, + show_user: true, + }, + { + name: 'fips_enabled', + type: 'bool', + title: 'Enable S3 FIPS', + description: + 'Enabling this option changes the service name from `s3` to `s3-fips` for connecting to the correct service endpoint.', + multi: false, + required: false, + show_user: false, + default: false, + }, + ], + template_path: 's3.yml.hbs', + title: 'AWS s3access logs', + description: 'Collect AWS s3access logs using s3 input', + enabled: true, + }, + ], + package: 'aws', + path: 's3access', + }, + { + type: 'metrics', + dataset: 'aws.sns', + title: 'AWS SNS metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '5m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS SNS metrics', + description: 'Collect AWS SNS metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'sns', + }, + { + type: 'metrics', + dataset: 'aws.sqs', + title: 'AWS SQS metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '5m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS SQS metrics', + description: 'Collect AWS SQS metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'sqs', + }, + { + type: 'metrics', + dataset: 'aws.transitgateway', + title: 'AWS Transit Gateway metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS Transit Gateway metrics', + description: 'Collect AWS Transit Gateway metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'transitgateway', + }, + { + type: 'metrics', + dataset: 'aws.usage', + title: 'AWS usage metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS usage metrics', + description: 'Collect AWS usage metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'usage', + }, + { + type: 'logs', + dataset: 'aws.vpcflow', + title: 'AWS vpcflow logs', + release: 'beta', + ingest_pipeline: 'default', + streams: [ + { + input: 's3', + vars: [ + { + name: 'visibility_timeout', + type: 'text', + title: 'Visibility Timeout', + description: + 'The duration that the received messages are hidden from subsequent retrieve requests after being retrieved by a ReceiveMessage request. The maximum is 12 hours.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'api_timeout', + type: 'text', + title: 'API Timeout', + description: + 'The maximum duration of AWS API can take. The maximum is half of the visibility timeout value.', + multi: false, + required: false, + show_user: false, + }, + { + name: 'queue_url', + type: 'text', + title: 'Queue URL', + description: 'URL of the AWS SQS queue that messages will be received from.', + multi: false, + required: true, + show_user: true, + }, + { + name: 'fips_enabled', + type: 'bool', + title: 'Enable S3 FIPS', + description: + 'Enabling this option changes the service name from `s3` to `s3-fips` for connecting to the correct service endpoint.', + multi: false, + required: false, + show_user: false, + default: false, + }, + ], + template_path: 's3.yml.hbs', + title: 'AWS vpcflow logs', + description: 'Collect AWS vpcflow logs using s3 input', + enabled: true, + }, + ], + package: 'aws', + path: 'vpcflow', + }, + { + type: 'metrics', + dataset: 'aws.vpn', + title: 'AWS VPN metrics', + release: 'beta', + streams: [ + { + input: 'aws/metrics', + vars: [ + { + name: 'period', + type: 'text', + title: 'Period', + multi: false, + required: true, + show_user: true, + default: '1m', + }, + { + name: 'regions', + type: 'text', + title: 'Regions', + multi: true, + required: false, + show_user: true, + }, + { + name: 'latency', + type: 'text', + title: 'Latency', + multi: false, + required: false, + show_user: false, + }, + { + name: 'tags_filter', + type: 'yaml', + title: 'Tags Filter', + multi: false, + required: false, + show_user: false, + default: '# - key: "created-by"\n # value: "foo"\n', + }, + ], + template_path: 'stream.yml.hbs', + title: 'AWS VPN metrics', + description: 'Collect AWS VPN metrics', + enabled: true, + }, + ], + package: 'aws', + path: 'vpn', + }, + ], + owner: { + github: 'elastic/integrations', + }, + vars: [ + { + name: 'shared_credential_file', + type: 'text', + title: 'Shared Credential File', + description: 'Directory of the shared credentials file', + multi: false, + required: false, + show_user: false, + }, + { + name: 'credential_profile_name', + type: 'text', + title: 'Credential Profile Name', + multi: false, + required: false, + show_user: true, + }, + { + name: 'access_key_id', + type: 'text', + title: 'Access Key ID', + multi: false, + required: false, + show_user: false, + }, + { + name: 'secret_access_key', + type: 'text', + title: 'Secret Access Key', + multi: false, + required: false, + show_user: false, + }, + { + name: 'session_token', + type: 'text', + title: 'Session Token', + multi: false, + required: false, + show_user: false, + }, + { + name: 'role_arn', + type: 'text', + title: 'Role ARN', + multi: false, + required: false, + show_user: false, + }, + { + name: 'endpoint', + type: 'text', + title: 'Endpoint', + description: 'URL of the entry point for an AWS web service', + multi: false, + required: false, + show_user: false, + default: 'amazonaws.com', + }, + ], + latestVersion: '0.5.3', + removable: true, + status: 'not_installed', +}; + +export const INVALID_AWS_POLICY = { + name: 'aws-1', + namespace: 'default', + package: { name: 'aws', title: 'AWS', version: '0.5.3' }, + enabled: true, + policy_id: 'some-agent-policy-id', + output_id: 'some-output-id', + inputs: [ + { + type: 'aws/metrics', + policy_template: 'billing', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.billing' }, + vars: { + period: { value: '12h', type: 'text' }, + latency: { type: 'text' }, + 'cost_explorer_config.group_by_dimension_keys': { + value: ['AZ', 'INSTANCE_TYPE', 'SERVICE'], + type: 'text', + }, + 'cost_explorer_config.group_by_tag_keys': { value: ['aws:createdBy'], type: 'text' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'cloudtrail', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.cloudtrail' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'httpjson', + policy_template: 'cloudtrail', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { type: 'logs', dataset: 'aws.cloudtrail' }, + vars: { + url: { value: 'https://server.example.com:8089', type: 'text' }, + username: { type: 'text' }, + password: { type: 'password' }, + ssl: { type: 'yaml' }, + interval: { value: '10s', type: 'text' }, + search: { value: 'search sourcetype=aws:cloudtrail', type: 'text' }, + tags: { value: ['forwarded'], type: 'text' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'cloudwatch', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.cloudwatch_logs' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'cloudwatch', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.cloudwatch_metrics' }, + vars: { + period: { value: '300s', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + metrics: { + value: + '- namespace: AWS/EC2\n resource_type: ec2:instance\n name:\n - CPUUtilization\n - DiskWriteOps\n statistic:\n - Average\n - Maximum\n # dimensions:\n # - name: InstanceId\n # value: i-123456\n # tags:\n # - key: created-by\n # value: foo\n', + type: 'yaml', + }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'dynamodb', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.dynamodb' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'ebs', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.ebs' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'ec2', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.ec2_logs' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'ec2', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.ec2_metrics' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'elb', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.elb_logs' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'elb', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.elb_metrics' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'lambda', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.lambda' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'natgateway', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.natgateway' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'rds', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.rds' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 's3', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.s3access' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 's3', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.s3_daily_storage' }, + vars: { + period: { value: '24h', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.s3_request' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'sns', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.sns' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'sqs', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.sqs' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'transitgateway', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.transitgateway' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'usage', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.usage' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'vpcflow', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.vpcflow' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'vpn', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.vpn' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + ], + vars: { + shared_credential_file: { type: 'text' }, + credential_profile_name: { type: 'text' }, + access_key_id: { type: 'text' }, + secret_access_key: { type: 'text' }, + session_token: { type: 'text' }, + role_arn: { type: 'text' }, + endpoint: { value: 'amazonaws.com', type: 'text' }, + }, +}; + +export const VALID_AWS_POLICY = { + name: 'aws-1', + namespace: 'default', + package: { name: 'aws', title: 'AWS', version: '0.5.3' }, + enabled: true, + policy_id: 'some-agent-policy-id', + output_id: 'some-output-id', + inputs: [ + { + type: 'aws/metrics', + policy_template: 'billing', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.billing' }, + vars: { + period: { value: '12h', type: 'text' }, + latency: { type: 'text' }, + 'cost_explorer_config.group_by_dimension_keys': { + value: ['AZ', 'INSTANCE_TYPE', 'SERVICE'], + type: 'text', + }, + 'cost_explorer_config.group_by_tag_keys': { value: ['aws:createdBy'], type: 'text' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'cloudtrail', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.cloudtrail' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text', value: 'http://localhost' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'httpjson', + policy_template: 'cloudtrail', + enabled: false, + streams: [ + { + enabled: false, + data_stream: { type: 'logs', dataset: 'aws.cloudtrail' }, + vars: { + url: { value: 'https://server.example.com:8089', type: 'text' }, + username: { type: 'text' }, + password: { type: 'password' }, + ssl: { type: 'yaml' }, + interval: { value: '10s', type: 'text' }, + search: { value: 'search sourcetype=aws:cloudtrail', type: 'text' }, + tags: { value: ['forwarded'], type: 'text' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'cloudwatch', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.cloudwatch_logs' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text', value: 'http://localhost' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'cloudwatch', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.cloudwatch_metrics' }, + vars: { + period: { value: '300s', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + metrics: { + value: + '- namespace: AWS/EC2\n resource_type: ec2:instance\n name:\n - CPUUtilization\n - DiskWriteOps\n statistic:\n - Average\n - Maximum\n # dimensions:\n # - name: InstanceId\n # value: i-123456\n # tags:\n # - key: created-by\n # value: foo\n', + type: 'yaml', + }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'dynamodb', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.dynamodb' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'ebs', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.ebs' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'ec2', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.ec2_logs' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text', value: 'http://localhost' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'ec2', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.ec2_metrics' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'elb', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.elb_logs' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text', value: 'http://localhost' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'elb', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.elb_metrics' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'lambda', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.lambda' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'natgateway', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.natgateway' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'rds', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.rds' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 's3', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.s3access' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text', value: 'http://localhost' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 's3', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.s3_daily_storage' }, + vars: { + period: { value: '24h', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.s3_request' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'sns', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.sns' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'sqs', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.sqs' }, + vars: { + period: { value: '5m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'transitgateway', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.transitgateway' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'usage', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.usage' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + }, + }, + ], + }, + { + type: 's3', + policy_template: 'vpcflow', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'logs', dataset: 'aws.vpcflow' }, + vars: { + visibility_timeout: { type: 'text' }, + api_timeout: { type: 'text' }, + queue_url: { type: 'text', value: 'http://localhost' }, + fips_enabled: { value: false, type: 'bool' }, + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'vpn', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { type: 'metrics', dataset: 'aws.vpn' }, + vars: { + period: { value: '1m', type: 'text' }, + regions: { value: [], type: 'text' }, + latency: { type: 'text' }, + tags_filter: { value: '# - key: "created-by"\n # value: "foo"\n', type: 'yaml' }, + }, + }, + ], + }, + ], + vars: { + shared_credential_file: { type: 'text' }, + credential_profile_name: { type: 'text' }, + access_key_id: { type: 'text' }, + secret_access_key: { type: 'text' }, + session_token: { type: 'text' }, + role_arn: { type: 'text' }, + endpoint: { value: 'amazonaws.com', type: 'text' }, + }, +}; diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 1fea5033e645c0..86361ae1633995 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -7,7 +7,11 @@ export * from './routes'; export * as AgentStatusKueryHelper from './agent_status'; -export { packageToPackagePolicyInputs, packageToPackagePolicy } from './package_to_package_policy'; +export { + packageToPackagePolicyInputs, + packageToPackagePolicy, + getStreamsForInputType, +} from './package_to_package_policy'; export { storedPackagePoliciesToAgentInputs } from './package_policies_to_agent_inputs'; export { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml'; export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limited_package'; @@ -17,3 +21,12 @@ export { isDiffPathProtocol } from './is_diff_path_protocol'; export { LicenseService } from './license'; export { isAgentUpgradeable } from './is_agent_upgradeable'; export { doesPackageHaveIntegrations } from './packages_with_integrations'; +export { + PackagePolicyValidationResults, + PackagePolicyConfigValidationResults, + PackagePolicyInputValidationResults, + validatePackagePolicy, + validatePackagePolicyConfig, + validationHasErrors, + countValidationErrors, +} from './validate_package_policy'; diff --git a/x-pack/plugins/fleet/common/services/limited_package.ts b/x-pack/plugins/fleet/common/services/limited_package.ts index e247b61869de12..601f680c8bf039 100644 --- a/x-pack/plugins/fleet/common/services/limited_package.ts +++ b/x-pack/plugins/fleet/common/services/limited_package.ts @@ -7,9 +7,10 @@ import type { PackageInfo, AgentPolicy, PackagePolicy } from '../types'; -// Assume packages only ever include 1 config template for now export const isPackageLimited = (packageInfo: PackageInfo): boolean => { - return packageInfo.policy_templates?.[0]?.multiple === false; + return (packageInfo.policy_templates || []).some( + (policyTemplate) => policyTemplate.multiple === false + ); }; export const doesAgentPolicyAlreadyIncludePackage = ( diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index 65b853ed5b38ff..0e5cab08edfb79 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -8,6 +8,7 @@ import type { PackageInfo } from '../types'; import { packageToPackagePolicy, packageToPackagePolicyInputs } from './package_to_package_policy'; +import { AWS_PACKAGE } from './fixtures/aws_package'; describe('Fleet - packageToPackagePolicy', () => { const mockPackage: PackageInfo = { @@ -59,7 +60,7 @@ describe('Fleet - packageToPackagePolicy', () => { expect( packageToPackagePolicyInputs(({ ...mockPackage, - policy_templates: [{ inputs: [] }], + policy_templates: [{ name: 'test_template', inputs: [] }], } as unknown) as PackageInfo) ).toEqual([]); }); @@ -68,17 +69,17 @@ describe('Fleet - packageToPackagePolicy', () => { expect( packageToPackagePolicyInputs(({ ...mockPackage, - policy_templates: [{ inputs: [{ type: 'foo' }] }], + policy_templates: [{ name: 'test_template', inputs: [{ type: 'foo' }] }], } as unknown) as PackageInfo) - ).toEqual([{ type: 'foo', enabled: true, streams: [] }]); + ).toEqual([{ type: 'foo', enabled: true, policy_template: 'test_template', streams: [] }]); expect( packageToPackagePolicyInputs(({ ...mockPackage, - policy_templates: [{ inputs: [{ type: 'foo' }, { type: 'bar' }] }], + policy_templates: [{ name: 'test_template', inputs: [{ type: 'foo' }, { type: 'bar' }] }], } as unknown) as PackageInfo) ).toEqual([ - { type: 'foo', enabled: true, streams: [] }, - { type: 'bar', enabled: true, streams: [] }, + { type: 'foo', enabled: true, policy_template: 'test_template', streams: [] }, + { type: 'bar', enabled: true, policy_template: 'test_template', streams: [] }, ]); }); @@ -91,24 +92,34 @@ describe('Fleet - packageToPackagePolicy', () => { { type: 'logs', dataset: 'bar', streams: [{ input: 'bar' }] }, { type: 'logs', dataset: 'bar2', streams: [{ input: 'bar' }] }, ], - policy_templates: [ - { - inputs: [{ type: 'foo' }, { type: 'bar' }], - }, - ], + policy_templates: [{ name: 'test_template', inputs: [{ type: 'foo' }, { type: 'bar' }] }], } as unknown) as PackageInfo) ).toEqual([ { type: 'foo', + policy_template: 'test_template', enabled: true, - streams: [{ enabled: true, data_stream: { dataset: 'foo', type: 'logs' } }], + streams: [ + { + enabled: true, + data_stream: { dataset: 'foo', type: 'logs' }, + }, + ], }, { type: 'bar', + policy_template: 'test_template', + enabled: true, streams: [ - { enabled: true, data_stream: { dataset: 'bar', type: 'logs' } }, - { enabled: true, data_stream: { dataset: 'bar2', type: 'logs' } }, + { + enabled: true, + data_stream: { dataset: 'bar', type: 'logs' }, + }, + { + enabled: true, + data_stream: { dataset: 'bar2', type: 'logs' }, + }, ], }, ]); @@ -145,15 +156,12 @@ describe('Fleet - packageToPackagePolicy', () => { ], }, ], - policy_templates: [ - { - inputs: [{ type: 'foo' }, { type: 'bar' }], - }, - ], + policy_templates: [{ name: 'test_template', inputs: [{ type: 'foo' }, { type: 'bar' }] }], } as unknown) as PackageInfo) ).toEqual([ { type: 'foo', + policy_template: 'test_template', enabled: true, streams: [ { @@ -165,6 +173,7 @@ describe('Fleet - packageToPackagePolicy', () => { }, { type: 'bar', + policy_template: 'test_template', enabled: true, streams: [ { @@ -236,6 +245,7 @@ describe('Fleet - packageToPackagePolicy', () => { ], policy_templates: [ { + name: 'test_template', inputs: [ { type: 'foo', @@ -262,6 +272,8 @@ describe('Fleet - packageToPackagePolicy', () => { ).toEqual([ { type: 'foo', + policy_template: 'test_template', + enabled: true, vars: { 'foo-input-var-name': { value: 'foo-input-var-value' }, @@ -280,6 +292,8 @@ describe('Fleet - packageToPackagePolicy', () => { }, { type: 'bar', + policy_template: 'test_template', + enabled: true, vars: { 'bar-input-var-name': { value: ['value1', 'value2'] }, @@ -304,6 +318,8 @@ describe('Fleet - packageToPackagePolicy', () => { }, { type: 'with-disabled-streams', + policy_template: 'test_template', + enabled: false, streams: [ { @@ -339,6 +355,7 @@ describe('Fleet - packageToPackagePolicy', () => { }, }); }); + it('returns package policy with custom name', () => { expect(packageToPackagePolicy(mockPackage, '1', '2', 'default', 'pkgPolicy-1')).toEqual({ policy_id: '1', @@ -354,6 +371,7 @@ describe('Fleet - packageToPackagePolicy', () => { }, }); }); + it('returns package policy with namespace and description', () => { expect( packageToPackagePolicy( @@ -379,6 +397,7 @@ describe('Fleet - packageToPackagePolicy', () => { }, }); }); + it('returns package policy with inputs', () => { const mockPackageWithPolicyTemplates = ({ ...mockPackage, @@ -401,5 +420,17 @@ describe('Fleet - packageToPackagePolicy', () => { }, }); }); + + it('returns package policy with multiple policy templates (aka has integrations', () => { + expect( + packageToPackagePolicy( + (AWS_PACKAGE as unknown) as PackageInfo, + 'some-agent-policy-id', + 'some-output-id', + 'default', + 'aws-1' + ) + ).toMatchSnapshot(); + }); }); }); diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts index 8f79e633eed0c2..0d40adb4bf7dc3 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts @@ -7,8 +7,8 @@ import type { PackageInfo, - RegistryPolicyTemplate, RegistryVarsEntry, + RegistryInput, RegistryStream, PackagePolicyConfigRecord, NewPackagePolicyInput, @@ -17,13 +17,20 @@ import type { PackagePolicyConfigRecordEntry, } from '../types'; -const getStreamsForInputType = ( +import { doesPackageHaveIntegrations } from './'; + +export const getStreamsForInputType = ( inputType: string, - packageInfo: PackageInfo + packageInfo: PackageInfo, + dataStreamPaths: string[] = [] ): Array => { const streams: Array = []; + const dataStreams = packageInfo.data_streams || []; + const dataStreamsToSearch = dataStreamPaths.length + ? dataStreams.filter((dataStream) => dataStreamPaths.includes(dataStream.path)) + : dataStreams; - (packageInfo.data_streams || []).forEach((dataStream) => { + dataStreamsToSearch.forEach((dataStream) => { (dataStream.streams || []).forEach((stream) => { if (stream.input === inputType) { streams.push({ @@ -59,48 +66,87 @@ const varsReducer = ( * This service creates a package policy inputs definition from defaults provided in package info */ export const packageToPackagePolicyInputs = ( - packageInfo: PackageInfo -): NewPackagePolicy['inputs'] => { - const inputs: NewPackagePolicy['inputs'] = []; - - // Assume package will only ever ship one package policy template for now - const packagePolicyTemplate: RegistryPolicyTemplate | null = - packageInfo.policy_templates && packageInfo.policy_templates[0] - ? packageInfo.policy_templates[0] - : null; - - // Create package policy input property - if (packagePolicyTemplate?.inputs?.length) { - // Map each package package policy input to agent policy package policy input - packagePolicyTemplate.inputs.forEach((packageInput) => { - // Map each package input stream into package policy input stream - const streams: NewPackagePolicyInputStream[] = getStreamsForInputType( - packageInput.type, - packageInfo - ).map((packageStream) => { - const stream: NewPackagePolicyInputStream = { - enabled: packageStream.enabled === false ? false : true, - data_stream: packageStream.data_stream, - }; - if (packageStream.vars && packageStream.vars.length) { - stream.vars = packageStream.vars.reduce(varsReducer, {}); - } - return stream; - }); - - const input: NewPackagePolicyInput = { - type: packageInput.type, - enabled: streams.length ? !!streams.find((stream) => stream.enabled) : true, - streams, + packageInfo: PackageInfo, + integrationToEnable?: string +): NewPackagePolicyInput[] => { + const hasIntegrations = doesPackageHaveIntegrations(packageInfo); + const inputs: NewPackagePolicyInput[] = []; + const packageInputsByPolicyTemplateAndType: { + [key: string]: RegistryInput & { data_streams?: string[]; policy_template: string }; + } = {}; + + packageInfo.policy_templates?.forEach((packagePolicyTemplate) => { + packagePolicyTemplate.inputs?.forEach((packageInput) => { + const inputKey = `${packagePolicyTemplate.name}-${packageInput.type}`; + const input = { + ...packageInput, + ...(packagePolicyTemplate.data_streams + ? { data_streams: packagePolicyTemplate.data_streams } + : {}), + policy_template: packagePolicyTemplate.name, }; + packageInputsByPolicyTemplateAndType[inputKey] = input; + }); + }); - if (packageInput.vars && packageInput.vars.length) { - input.vars = packageInput.vars.reduce(varsReducer, {}); - } + Object.values(packageInputsByPolicyTemplateAndType).forEach((packageInput) => { + const streamsForInput: NewPackagePolicyInputStream[] = []; + let varsForInput: PackagePolicyConfigRecord = {}; - inputs.push(input); + // Map each package input stream into package policy input stream + const streams = getStreamsForInputType( + packageInput.type, + packageInfo, + packageInput.data_streams + ).map((packageStream) => { + const stream: NewPackagePolicyInputStream = { + enabled: packageStream.enabled === false ? false : true, + data_stream: packageStream.data_stream, + }; + if (packageStream.vars && packageStream.vars.length) { + stream.vars = packageStream.vars.reduce(varsReducer, {}); + } + return stream; }); - } + + // If non-integration package, collect input-level vars, otherwise skip them, + // we do not support input-level vars for packages with integrations yet) + if (packageInput.vars?.length && !hasIntegrations) { + varsForInput = packageInput.vars.reduce(varsReducer, {}); + } + + streamsForInput.push(...streams); + + // Check if we should enable this input by the streams below it + // Enable it if at least one of its streams is enabled + let enableInput = streamsForInput.length + ? !!streamsForInput.find((stream) => stream.enabled) + : true; + + // If we are wanting to enabling this input, check if we only want + // to enable specific integrations (aka `policy_template`s) + if ( + enableInput && + hasIntegrations && + integrationToEnable && + integrationToEnable !== packageInput.policy_template + ) { + enableInput = false; + } + + const input: NewPackagePolicyInput = { + type: packageInput.type, + policy_template: packageInput.policy_template, + enabled: enableInput, + streams: streamsForInput, + }; + + if (Object.keys(varsForInput).length) { + input.vars = varsForInput; + } + + inputs.push(input); + }); return inputs; }; @@ -119,7 +165,8 @@ export const packageToPackagePolicy = ( outputId: string, namespace: string = '', packagePolicyName?: string, - description?: string + description?: string, + integrationToEnable?: string ): NewPackagePolicy => { const packagePolicy: NewPackagePolicy = { name: packagePolicyName || `${packageInfo.name}-1`, @@ -133,7 +180,8 @@ export const packageToPackagePolicy = ( enabled: true, policy_id: agentPolicyId, output_id: outputId, - inputs: packageToPackagePolicyInputs(packageInfo), + inputs: packageToPackagePolicyInputs(packageInfo, integrationToEnable), + vars: undefined, }; if (packageInfo.vars?.length) { diff --git a/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts new file mode 100644 index 00000000000000..95dbf156040a17 --- /dev/null +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts @@ -0,0 +1,635 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { installationStatuses } from '../constants'; +import type { PackageInfo, NewPackagePolicy, RegistryPolicyTemplate } from '../types'; + +import { validatePackagePolicy, validationHasErrors } from './validate_package_policy'; +import { AWS_PACKAGE, INVALID_AWS_POLICY, VALID_AWS_POLICY } from './fixtures/aws_package'; + +describe('Fleet - validatePackagePolicy()', () => { + describe('works for packages with single policy template (aka no integrations)', () => { + const mockPackage = ({ + name: 'mock-package', + title: 'Mock package', + version: '0.0.0', + description: 'description', + type: 'mock', + categories: [], + requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } }, + format_version: '', + download: '', + path: '', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + 'index-pattern': [], + }, + }, + status: installationStatuses.NotInstalled, + data_streams: [ + { + dataset: 'foo', + streams: [ + { + input: 'foo', + title: 'Foo', + vars: [{ name: 'var-name', type: 'yaml' }], + }, + ], + }, + { + dataset: 'bar', + streams: [ + { + input: 'bar', + title: 'Bar', + vars: [{ name: 'var-name', type: 'yaml', required: true }], + }, + { + input: 'with-no-stream-vars', + title: 'Bar stream no vars', + enabled: true, + }, + ], + }, + { + dataset: 'bar2', + streams: [ + { + input: 'bar', + title: 'Bar 2', + vars: [{ default: 'bar2-var-value', name: 'var-name', type: 'text' }], + }, + ], + }, + { + dataset: 'disabled', + streams: [ + { + input: 'with-disabled-streams', + title: 'Disabled', + enabled: false, + vars: [{ multi: true, required: true, name: 'var-name', type: 'text' }], + }, + ], + }, + { + dataset: 'disabled2', + streams: [ + { + input: 'with-disabled-streams', + title: 'Disabled 2', + enabled: false, + }, + ], + }, + ], + policy_templates: [ + { + name: 'pkgPolicy1', + title: 'Package policy 1', + description: 'test package policy', + inputs: [ + { + type: 'foo', + title: 'Foo', + vars: [ + { default: 'foo-input-var-value', name: 'foo-input-var-name', type: 'text' }, + { + default: 'foo-input2-var-value', + name: 'foo-input2-var-name', + required: true, + type: 'text', + }, + { name: 'foo-input3-var-name', type: 'text', required: true, multi: true }, + ], + }, + { + type: 'bar', + title: 'Bar', + vars: [ + { + default: ['value1', 'value2'], + name: 'bar-input-var-name', + type: 'text', + multi: true, + }, + { name: 'bar-input2-var-name', required: true, type: 'text' }, + ], + }, + { + type: 'with-no-config-or-streams', + title: 'With no config or streams', + }, + { + type: 'with-disabled-streams', + title: 'With disabled streams', + }, + { + type: 'with-no-stream-vars', + enabled: true, + vars: [{ required: true, name: 'var-name', type: 'text' }], + }, + ], + }, + ], + } as unknown) as PackageInfo; + + const validPackagePolicy: NewPackagePolicy = { + name: 'pkgPolicy1-1', + namespace: 'default', + policy_id: 'test-policy', + enabled: true, + output_id: 'test-output', + inputs: [ + { + type: 'foo', + policy_template: 'pkgPolicy1', + enabled: true, + vars: { + 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' }, + 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' }, + 'foo-input3-var-name': { value: ['test'], type: 'text' }, + }, + streams: [ + { + data_stream: { dataset: 'foo', type: 'logs' }, + enabled: true, + vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + }, + ], + }, + { + type: 'bar', + policy_template: 'pkgPolicy1', + enabled: true, + vars: { + 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' }, + 'bar-input2-var-name': { value: 'test', type: 'text' }, + }, + streams: [ + { + data_stream: { dataset: 'bar', type: 'logs' }, + enabled: true, + vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + }, + { + data_stream: { dataset: 'bar2', type: 'logs' }, + enabled: true, + vars: { 'var-name': { value: undefined, type: 'text' } }, + }, + ], + }, + { + type: 'with-no-config-or-streams', + policy_template: 'pkgPolicy1', + enabled: true, + streams: [], + }, + { + type: 'with-disabled-streams', + policy_template: 'pkgPolicy1', + enabled: true, + streams: [ + { + data_stream: { dataset: 'disabled', type: 'logs' }, + enabled: false, + vars: { 'var-name': { value: undefined, type: 'text' } }, + }, + { + data_stream: { dataset: 'disabled2', type: 'logs' }, + enabled: false, + }, + ], + }, + { + type: 'with-no-stream-vars', + policy_template: 'pkgPolicy1', + enabled: true, + vars: { + 'var-name': { value: 'test', type: 'text' }, + }, + streams: [ + { + data_stream: { dataset: 'with-no-stream-vars-bar', type: 'logs' }, + enabled: true, + }, + ], + }, + ], + }; + + const invalidPackagePolicy: NewPackagePolicy = { + ...validPackagePolicy, + name: '', + inputs: [ + { + type: 'foo', + policy_template: 'pkgPolicy1', + enabled: true, + vars: { + 'foo-input-var-name': { value: undefined, type: 'text' }, + 'foo-input2-var-name': { value: '', type: 'text' }, + 'foo-input3-var-name': { value: [], type: 'text' }, + }, + streams: [ + { + data_stream: { dataset: 'foo', type: 'logs' }, + enabled: true, + vars: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, + }, + ], + }, + { + type: 'bar', + policy_template: 'pkgPolicy1', + enabled: true, + vars: { + 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' }, + 'bar-input2-var-name': { value: undefined, type: 'text' }, + }, + streams: [ + { + data_stream: { dataset: 'bar', type: 'logs' }, + enabled: true, + vars: { 'var-name': { value: ' \n\n', type: 'yaml' } }, + }, + { + data_stream: { dataset: 'bar2', type: 'logs' }, + enabled: true, + vars: { 'var-name': { value: undefined, type: 'text' } }, + }, + ], + }, + { + type: 'with-no-config-or-streams', + policy_template: 'pkgPolicy1', + enabled: true, + streams: [], + }, + { + type: 'with-disabled-streams', + policy_template: 'pkgPolicy1', + enabled: true, + streams: [ + { + data_stream: { dataset: 'disabled', type: 'logs' }, + enabled: false, + vars: { + 'var-name': { + value: 'invalid value but not checked due to not enabled', + type: 'text', + }, + }, + }, + { + data_stream: { dataset: 'disabled2', type: 'logs' }, + enabled: false, + }, + ], + }, + { + type: 'with-no-stream-vars', + policy_template: 'pkgPolicy1', + enabled: true, + vars: { + 'var-name': { value: undefined, type: 'text' }, + }, + streams: [ + { + data_stream: { dataset: 'with-no-stream-vars-bar', type: 'logs' }, + enabled: true, + }, + ], + }, + ], + }; + + const noErrorsValidationResults = { + name: null, + description: null, + namespace: null, + inputs: { + foo: { + vars: { + 'foo-input-var-name': null, + 'foo-input2-var-name': null, + 'foo-input3-var-name': null, + }, + streams: { foo: { vars: { 'var-name': null } } }, + }, + bar: { + vars: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, + streams: { + bar: { vars: { 'var-name': null } }, + bar2: { vars: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { + disabled: { + vars: { 'var-name': null }, + }, + disabled2: {}, + }, + }, + 'with-no-stream-vars': { + streams: { + 'with-no-stream-vars-bar': {}, + }, + vars: { 'var-name': null }, + }, + }, + }; + + it('returns no errors for valid package policy', () => { + expect(validatePackagePolicy(validPackagePolicy, mockPackage)).toEqual( + noErrorsValidationResults + ); + }); + + it('returns errors for invalid package policy', () => { + expect(validatePackagePolicy(invalidPackagePolicy, mockPackage)).toEqual({ + name: ['Name is required'], + description: null, + namespace: null, + inputs: { + foo: { + vars: { + 'foo-input-var-name': null, + 'foo-input2-var-name': ['foo-input2-var-name is required'], + 'foo-input3-var-name': ['foo-input3-var-name is required'], + }, + streams: { foo: { vars: { 'var-name': ['Invalid YAML format'] } } }, + }, + bar: { + vars: { + 'bar-input-var-name': ['Invalid format'], + 'bar-input2-var-name': ['bar-input2-var-name is required'], + }, + streams: { + bar: { vars: { 'var-name': ['var-name is required'] } }, + bar2: { vars: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { + disabled: { vars: { 'var-name': null } }, + disabled2: {}, + }, + }, + 'with-no-stream-vars': { + vars: { + 'var-name': ['var-name is required'], + }, + streams: { 'with-no-stream-vars-bar': {} }, + }, + }, + }); + }); + + it('returns no errors for disabled inputs', () => { + const disabledInputs = invalidPackagePolicy.inputs.map((input) => ({ + ...input, + enabled: false, + })); + expect( + validatePackagePolicy({ ...validPackagePolicy, inputs: disabledInputs }, mockPackage) + ).toEqual(noErrorsValidationResults); + }); + + it('returns only package policy and input-level errors for disabled streams', () => { + const inputsWithDisabledStreams = invalidPackagePolicy.inputs.map((input) => + input.streams + ? { + ...input, + streams: input.streams.map((stream) => ({ ...stream, enabled: false })), + } + : input + ); + expect( + validatePackagePolicy( + { ...invalidPackagePolicy, inputs: inputsWithDisabledStreams }, + mockPackage + ) + ).toEqual({ + name: ['Name is required'], + description: null, + namespace: null, + inputs: { + foo: { + vars: { + 'foo-input-var-name': null, + 'foo-input2-var-name': ['foo-input2-var-name is required'], + 'foo-input3-var-name': ['foo-input3-var-name is required'], + }, + streams: { foo: { vars: { 'var-name': null } } }, + }, + bar: { + vars: { + 'bar-input-var-name': ['Invalid format'], + 'bar-input2-var-name': ['bar-input2-var-name is required'], + }, + streams: { + bar: { vars: { 'var-name': null } }, + bar2: { vars: { 'var-name': null } }, + }, + }, + 'with-disabled-streams': { + streams: { + disabled: { + vars: { 'var-name': null }, + }, + disabled2: {}, + }, + }, + 'with-no-stream-vars': { + vars: { + 'var-name': ['var-name is required'], + }, + streams: { 'with-no-stream-vars-bar': {} }, + }, + }, + }); + }); + + it('returns no errors for packages with no package policies', () => { + expect( + validatePackagePolicy(validPackagePolicy, { + ...mockPackage, + policy_templates: undefined, + }) + ).toEqual({ + name: null, + description: null, + namespace: null, + inputs: null, + }); + expect( + validatePackagePolicy(validPackagePolicy, { + ...mockPackage, + policy_templates: [], + }) + ).toEqual({ + name: null, + description: null, + namespace: null, + inputs: null, + }); + }); + + it('returns no errors for packages with no inputs', () => { + expect( + validatePackagePolicy(validPackagePolicy, { + ...mockPackage, + policy_templates: [{} as RegistryPolicyTemplate], + }) + ).toEqual({ + name: null, + description: null, + namespace: null, + inputs: null, + }); + expect( + validatePackagePolicy(validPackagePolicy, { + ...mockPackage, + policy_templates: [({ inputs: [] } as unknown) as RegistryPolicyTemplate], + }) + ).toEqual({ + name: null, + description: null, + namespace: null, + inputs: null, + }); + }); + }); + + describe('works for packages with multiple policy templates (aka integrations)', () => { + it('returns errors for invalid package policy', () => { + expect( + validatePackagePolicy( + INVALID_AWS_POLICY as NewPackagePolicy, + (AWS_PACKAGE as unknown) as PackageInfo + ) + ).toMatchSnapshot(); + }); + + it('returns no errors for valid package policy', () => { + expect( + validationHasErrors( + validatePackagePolicy( + VALID_AWS_POLICY as NewPackagePolicy, + (AWS_PACKAGE as unknown) as PackageInfo + ) + ) + ).toBe(false); + }); + }); +}); + +describe('Fleet - validationHasErrors()', () => { + it('returns true for stream validation results with errors', () => { + expect( + validationHasErrors({ + vars: { foo: ['foo error'], bar: null }, + }) + ).toBe(true); + }); + + it('returns false for stream validation results with no errors', () => { + expect( + validationHasErrors({ + vars: { foo: null, bar: null }, + }) + ).toBe(false); + }); + + it('returns true for input validation results with errors', () => { + expect( + validationHasErrors({ + vars: { foo: ['foo error'], bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, + }) + ).toBe(true); + expect( + validationHasErrors({ + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, + }) + ).toBe(true); + }); + + it('returns false for input validation results with no errors', () => { + expect( + validationHasErrors({ + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, + }) + ).toBe(false); + }); + + it('returns true for package policy validation results with errors', () => { + expect( + validationHasErrors({ + name: ['name error'], + description: null, + namespace: null, + inputs: { + input1: { + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(true); + expect( + validationHasErrors({ + name: null, + description: null, + namespace: null, + inputs: { + input1: { + vars: { foo: ['foo error'], bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(true); + expect( + validationHasErrors({ + name: null, + description: null, + namespace: null, + inputs: { + input1: { + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, + }, + }, + }) + ).toBe(true); + }); + + it('returns false for package policy validation results with no errors', () => { + expect( + validationHasErrors({ + name: null, + description: null, + namespace: null, + inputs: { + input1: { + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, + }, + }, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.ts similarity index 76% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts rename to x-pack/plugins/fleet/common/services/validate_package_policy.ts index de1b8df9f95974..b8673aa8b2301d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.ts @@ -5,21 +5,22 @@ * 2.0. */ +import { getFlattenedObject } from '@kbn/std'; import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; import { keyBy } from 'lodash'; -import { getFlattenedObject, isValidNamespace } from '../../../../services'; import type { NewPackagePolicy, PackagePolicyInput, PackagePolicyInputStream, PackagePolicyConfigRecordEntry, PackageInfo, - RegistryInput, RegistryStream, RegistryVarsEntry, -} from '../../../../types'; +} from '../types'; + +import { isValidNamespace, doesPackageHaveIntegrations } from './'; type Errors = string[] | null; @@ -48,6 +49,7 @@ export const validatePackagePolicy = ( packagePolicy: NewPackagePolicy, packageInfo: PackageInfo ): PackagePolicyValidationResults => { + const hasIntegrations = doesPackageHaveIntegrations(packageInfo); const validationResults: PackagePolicyValidationResults = { name: null, description: null, @@ -81,54 +83,61 @@ export const validatePackagePolicy = ( if ( !packageInfo.policy_templates || packageInfo.policy_templates.length === 0 || - !packageInfo.policy_templates[0] || - !packageInfo.policy_templates[0].inputs || - packageInfo.policy_templates[0].inputs.length === 0 + !packageInfo.policy_templates.find( + (policyTemplate) => policyTemplate.inputs && policyTemplate.inputs.length > 0 + ) ) { validationResults.inputs = null; return validationResults; } - const registryInputsByType: Record< - string, - RegistryInput - > = packageInfo.policy_templates[0].inputs.reduce((inputs, registryInput) => { - inputs[registryInput.type] = registryInput; - return inputs; - }, {} as Record); - - const registryStreamsByDataset: Record = ( - packageInfo.data_streams || [] - ).reduce((dataStreams, registryDataStream) => { - dataStreams[registryDataStream.dataset] = registryDataStream.streams || []; - return dataStreams; - }, {} as Record); - - // Validate each package policy input with either its own config fields or streams + // Build cache for fast var definition lookup + const inputVarDefsByPolicyTemplateAndType = packageInfo.policy_templates.reduce< + Record> + >((varDefs, policyTemplate) => { + (policyTemplate.inputs || []).forEach((input) => { + const varDefKey = hasIntegrations ? `${policyTemplate.name}-${input.type}` : input.type; + if ((input.vars || []).length) { + varDefs[varDefKey] = keyBy(input.vars || [], 'name'); + } + }); + return varDefs; + }, {}); + const streamsByDatasetAndInput = (packageInfo.data_streams || []).reduce< + Record + >((streams, dataStream) => { + dataStream.streams?.forEach((stream) => { + streams[`${dataStream.dataset}-${stream.input}`] = stream; + }); + return streams; + }, {}); + const streamVarDefsByDatasetAndInput = Object.entries(streamsByDatasetAndInput).reduce< + Record> + >((varDefs, [path, stream]) => { + varDefs[path] = keyBy(stream.vars || [], 'name'); + return varDefs; + }, {}); + + // Validate each package policy input with either its own var fields and stream vars packagePolicy.inputs.forEach((input) => { if (!input.vars && !input.streams) { return; } - + const inputKey = hasIntegrations ? `${input.policy_template}-${input.type}` : input.type; const inputValidationResults: PackagePolicyInputValidationResults = { vars: undefined, streams: {}, }; - const inputVarsByName = (registryInputsByType[input.type].vars || []).reduce( - (vars, registryVar) => { - vars[registryVar.name] = registryVar; - return vars; - }, - {} as Record - ); - - // Validate input-level config fields - const inputConfigs = Object.entries(input.vars || {}); - if (inputConfigs.length) { - inputValidationResults.vars = inputConfigs.reduce((results, [name, configEntry]) => { + // Validate input-level var fields + const inputVars = Object.entries(input.vars || {}); + if (inputVars.length) { + inputValidationResults.vars = inputVars.reduce((results, [name, configEntry]) => { results[name] = input.enabled - ? validatePackagePolicyConfig(configEntry, inputVarsByName[name]) + ? validatePackagePolicyConfig( + configEntry, + inputVarDefsByPolicyTemplateAndType[inputKey][name] + ) : null; return results; }, {} as ValidationEntry); @@ -136,28 +145,20 @@ export const validatePackagePolicy = ( delete inputValidationResults.vars; } - // Validate each input stream with config fields + // Validate each input stream with var definitions if (input.streams.length) { input.streams.forEach((stream) => { const streamValidationResults: PackagePolicyConfigValidationResults = {}; + const streamVarDefs = + streamVarDefsByDatasetAndInput[`${stream.data_stream.dataset}-${input.type}`]; // Validate stream-level config fields if (stream.vars) { - const streamVarsByName = ( - ( - registryStreamsByDataset[stream.data_stream.dataset].find( - (registryStream) => registryStream.input === input.type - ) || {} - ).vars || [] - ).reduce((vars, registryVar) => { - vars[registryVar.name] = registryVar; - return vars; - }, {} as Record); streamValidationResults.vars = Object.entries(stream.vars).reduce( (results, [name, configEntry]) => { results[name] = - input.enabled && stream.enabled - ? validatePackagePolicyConfig(configEntry, streamVarsByName[name]) + streamVarDefs[name] && input.enabled && stream.enabled + ? validatePackagePolicyConfig(configEntry, streamVarDefs[name]) : null; return results; }, @@ -172,7 +173,7 @@ export const validatePackagePolicy = ( } if (inputValidationResults.vars || inputValidationResults.streams) { - validationResults.inputs![input.type] = inputValidationResults; + validationResults.inputs![inputKey] = inputValidationResults; } }); diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 753100f6225567..a9393abcc57ef8 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -61,9 +61,9 @@ export interface FullAgentPolicyInput { } export interface FullAgentPolicyOutputPermissions { - [role: string]: { - cluster: string[]; - indices: Array<{ + [packagePolicyName: string]: { + cluster?: string[]; + indices?: Array<{ names: string[]; privileges: string[]; }>; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5551453b8975c2..0ef9f8b7ace36a 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -276,6 +276,7 @@ export enum RegistryDataStreamKeys { ingest_pipeline = 'ingest_pipeline', elasticsearch = 'elasticsearch', dataset_is_prefix = 'dataset_is_prefix', + permissions = 'permissions', } export interface RegistryDataStream { @@ -291,6 +292,7 @@ export interface RegistryDataStream { [RegistryDataStreamKeys.ingest_pipeline]?: string; [RegistryDataStreamKeys.elasticsearch]?: RegistryElasticsearch; [RegistryDataStreamKeys.dataset_is_prefix]?: boolean; + [RegistryDataStreamKeys.permissions]?: RegistryDataStreamPermissions; } export interface RegistryElasticsearch { @@ -298,6 +300,11 @@ export interface RegistryElasticsearch { 'index_template.mappings'?: object; } +export interface RegistryDataStreamPermissions { + cluster?: string[]; + indices?: string[]; +} + export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml' | 'string'; export enum RegistryVarsEntryKeys { name = 'name', diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index c0b74c2a7b025f..40c3a0c66f15cf 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -37,6 +37,7 @@ export interface PackagePolicyInputStream extends NewPackagePolicyInputStream { export interface NewPackagePolicyInput { type: string; + policy_template?: string; enabled: boolean; keep_enabled?: boolean; vars?: PackagePolicyConfigRecord; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/components/index.ts index ac5a78a4ea7095..5e927c5b0e3d6f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/components/index.ts @@ -7,5 +7,4 @@ export * from '../../../components'; -export * from './enrollment_instructions'; export * from './search_bar'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index eeb8f18d17d4f6..fd980475dc9194 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -91,10 +91,10 @@ const breadcrumbGetters: { }), }, ], - add_integration_to_policy: ({ pkgTitle, pkgkey }) => [ + add_integration_to_policy: ({ pkgTitle, pkgkey, integration }) => [ INTEGRATIONS_BASE_BREADCRUMB, { - href: pagePathGetters.integration_details_overview({ pkgkey })[1], + href: pagePathGetters.integration_details_overview({ pkgkey, integration })[1], text: pkgTitle, useIntegrationsBasePath: true, }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx index bdf49f44f4397a..798ed4f0381566 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx @@ -11,8 +11,7 @@ import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; import type { AgentPolicy } from '../../../types'; import { useCapabilities } from '../../../hooks'; -import { ContextMenuActions } from '../../../components'; -import { AgentEnrollmentFlyout } from '../../agents/components'; +import { AgentEnrollmentFlyout, ContextMenuActions } from '../../../components'; import { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout'; import { AgentPolicyCopyProvider } from './agent_policy_copy_provider'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts index 09282a7de7cb9b..439e474d416cb5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/index.ts @@ -11,8 +11,6 @@ export { LinkedAgentCount } from '../../../components'; export { AgentPolicyForm, agentPolicyFormValidation } from './agent_policy_form'; export { AgentPolicyCopyProvider } from './agent_policy_copy_provider'; export { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; -export { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout'; export { ConfirmDeployAgentPolicyModal } from './confirm_deploy_modal'; -export { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; export { AgentPolicyActionMenu } from './actions_menu'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index d2bc49bdf00e07..060c49a84c5aae 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { WithHeaderLayout } from '../../../../layouts'; -import type { AgentPolicy, PackageInfo } from '../../../../types'; +import type { AgentPolicy, PackageInfo, RegistryPolicyTemplate } from '../../../../types'; import { PackageIcon } from '../../../../components'; import type { CreatePackagePolicyFrom } from '../types'; @@ -29,6 +29,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ onCancel?: React.ReactEventHandler; agentPolicy?: AgentPolicy; packageInfo?: PackageInfo; + integrationInfo?: RegistryPolicyTemplate; 'data-test-subj'?: string; }> = memo( ({ @@ -37,6 +38,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ onCancel, agentPolicy, packageInfo, + integrationInfo, children, 'data-test-subj': dataTestSubj, }) => { @@ -47,8 +49,9 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ @@ -68,7 +71,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ id="xpack.fleet.createPackagePolicy.pageTitleWithPackageName" defaultMessage="Add {packageName} integration" values={{ - packageName: packageInfo.title, + packageName: integrationInfo?.title || packageInfo.title, }} /> )} @@ -98,7 +101,14 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ); - }, [dataTestSubj, from, packageInfo]); + }, [ + dataTestSubj, + from, + integrationInfo?.icons, + integrationInfo?.name, + integrationInfo?.title, + packageInfo, + ]); const pageDescription = useMemo(() => { return from === 'edit' || from === 'package-edit' ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 4444ca2c5904f3..be0dc817461099 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -58,9 +58,14 @@ const StepsWithLessPadding = styled(EuiSteps)` padding-bottom: ${(props) => props.theme.eui.paddingSizes.m}; } `; -interface RouteParameters { - policyId: string; + +interface AddToPolicyParams { pkgkey: string; + integration?: string; +} + +interface AddFromPolicyParams { + policyId: string; } export const CreatePackagePolicyPage: React.FunctionComponent = () => { @@ -68,15 +73,12 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { const { agents: { enabled: isFleetEnabled }, } = useConfig(); - const { - params: { policyId: routeParamsAgentPolicyId, pkgkey }, - } = useRouteMatch(); + const { params } = useRouteMatch(); const { getHref, getPath } = useLink(); const history = useHistory(); const handleNavigateTo = useNavigateToCallback(); const routeState = useIntraAppState(); - const from: CreatePackagePolicyFrom = routeParamsAgentPolicyId ? 'policy' : 'package'; - const policyId = from === 'policy' ? routeParamsAgentPolicyId : routeState?.agentPolicyId; + const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package'; // Agent policy and package info states const [agentPolicy, setAgentPolicy] = useState(); @@ -219,9 +221,11 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { return routeState.onCancelUrl; } return from === 'policy' - ? getHref('policy_details', { policyId: agentPolicyId || policyId! }) - : getHref('integration_details_overview', { pkgkey }); - }, [agentPolicyId, policyId, from, getHref, pkgkey, routeState]); + ? getHref('policy_details', { + policyId: agentPolicyId || (params as AddFromPolicyParams).policyId, + }) + : getHref('integration_details_overview', { pkgkey: (params as AddToPolicyParams).pkgkey }); + }, [agentPolicyId, params, from, getHref, routeState]); const cancelClickHandler: ReactEventHandler = useCallback( (ev) => { @@ -234,14 +238,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ); // Save package policy - const savePackagePolicy = async () => { + const savePackagePolicy = useCallback(async () => { setFormState('LOADING'); const result = await sendCreatePackagePolicy(packagePolicy); setFormState('SUBMITTED'); return result; - }; + }, [packagePolicy]); - const onSubmit = async () => { + const onSubmit = useCallback(async () => { if (formState === 'VALID' && hasErrors) { setFormState('INVALID'); return; @@ -259,8 +263,11 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { : routeState.onSaveNavigateTo ); } else { - // Assume agentPolicy has been set if we get this far since the form validity depends on it - history.push(getPath('policy_details', { policyId: agentPolicy?.id! || policyId! })); + history.push( + getPath('policy_details', { + policyId: agentPolicy?.id || (params as AddFromPolicyParams).policyId, + }) + ); } notifications.toasts.addSuccess({ @@ -294,28 +301,55 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { }); setFormState('VALID'); } - }; - - const layoutProps = { - from, - cancelUrl, - onCancel: cancelClickHandler, + }, [ + agentCount, agentPolicy, - packageInfo, - }; + formState, + getPath, + handleNavigateTo, + hasErrors, + history, + notifications.toasts, + packagePolicy.name, + params, + routeState, + savePackagePolicy, + ]); + + const integrationInfo = useMemo( + () => + (params as AddToPolicyParams).integration + ? packageInfo?.policy_templates?.find( + (policyTemplate) => policyTemplate.name === (params as AddToPolicyParams).integration + ) + : undefined, + [packageInfo?.policy_templates, params] + ); + + const layoutProps = useMemo( + () => ({ + from, + cancelUrl, + onCancel: cancelClickHandler, + agentPolicy, + packageInfo, + integrationInfo, + }), + [agentPolicy, cancelClickHandler, cancelUrl, from, integrationInfo, packageInfo] + ); const stepSelectAgentPolicy = useMemo( () => ( ), - [pkgkey, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId] + [params, updatePackageInfo, agentPolicy, updateAgentPolicy] ); const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); @@ -333,12 +367,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { updatePackagePolicy={updatePackagePolicy} validationResults={validationResults!} submitAttempted={formState === 'INVALID'} + integrationToEnable={integrationInfo?.name} /> {/* Only show the out-of-box configuration step if a UI extension is NOT registered */} {!ExtensionView && ( { updatePackagePolicy, validationResults, formState, + integrationInfo?.name, ExtensionView, handleExtensionViewOnChange, ] @@ -399,8 +436,9 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { {from === 'package' ? packageInfo && ( ) : agentPolicy && ( @@ -469,8 +507,13 @@ const PolicyBreadcrumb: React.FunctionComponent<{ const IntegrationBreadcrumb: React.FunctionComponent<{ pkgTitle: string; pkgkey: string; -}> = ({ pkgTitle, pkgkey }) => { - useBreadcrumbs('add_integration_to_policy', { pkgTitle, pkgkey }); + integration?: string; +}> = ({ pkgTitle, pkgkey, integration }) => { + useBreadcrumbs('add_integration_to_policy', { + pkgTitle, + pkgkey, + ...(integration ? { integration } : {}), + }); return null; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/index.ts index 0c2dc6c1c64912..0e1953316fd53c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/index.ts @@ -15,4 +15,4 @@ export { validatePackagePolicyConfig, validationHasErrors, countValidationErrors, -} from './validate_package_policy'; +} from '../../../../services'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts deleted file mode 100644 index 180a585a4305ac..00000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test.ts +++ /dev/null @@ -1,600 +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 { installationStatuses } from '../../../../../../../common/constants'; -import type { PackageInfo, NewPackagePolicy, RegistryPolicyTemplate } from '../../../../types'; - -import { validatePackagePolicy, validationHasErrors } from './validate_package_policy'; - -describe('Fleet - validatePackagePolicy()', () => { - const mockPackage = ({ - name: 'mock-package', - title: 'Mock package', - version: '0.0.0', - description: 'description', - type: 'mock', - categories: [], - requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } }, - format_version: '', - download: '', - path: '', - assets: { - kibana: { - dashboard: [], - visualization: [], - search: [], - 'index-pattern': [], - }, - }, - status: installationStatuses.NotInstalled, - data_streams: [ - { - dataset: 'foo', - streams: [ - { - input: 'foo', - title: 'Foo', - vars: [{ name: 'var-name', type: 'yaml' }], - }, - ], - }, - { - dataset: 'bar', - streams: [ - { - input: 'bar', - title: 'Bar', - vars: [{ name: 'var-name', type: 'yaml', required: true }], - }, - { - input: 'with-no-stream-vars', - title: 'Bar stream no vars', - enabled: true, - }, - ], - }, - { - dataset: 'bar2', - streams: [ - { - input: 'bar', - title: 'Bar 2', - vars: [{ default: 'bar2-var-value', name: 'var-name', type: 'text' }], - }, - ], - }, - { - dataset: 'disabled', - streams: [ - { - input: 'with-disabled-streams', - title: 'Disabled', - enabled: false, - vars: [{ multi: true, required: true, name: 'var-name', type: 'text' }], - }, - ], - }, - { - dataset: 'disabled2', - streams: [ - { - input: 'with-disabled-streams', - title: 'Disabled 2', - enabled: false, - }, - ], - }, - ], - policy_templates: [ - { - name: 'pkgPolicy1', - title: 'Package policy 1', - description: 'test package policy', - inputs: [ - { - type: 'foo', - title: 'Foo', - vars: [ - { default: 'foo-input-var-value', name: 'foo-input-var-name', type: 'text' }, - { - default: 'foo-input2-var-value', - name: 'foo-input2-var-name', - required: true, - type: 'text', - }, - { name: 'foo-input3-var-name', type: 'text', required: true, multi: true }, - ], - }, - { - type: 'bar', - title: 'Bar', - vars: [ - { - default: ['value1', 'value2'], - name: 'bar-input-var-name', - type: 'text', - multi: true, - }, - { name: 'bar-input2-var-name', required: true, type: 'text' }, - ], - }, - { - type: 'with-no-config-or-streams', - title: 'With no config or streams', - }, - { - type: 'with-disabled-streams', - title: 'With disabled streams', - }, - { - type: 'with-no-stream-vars', - enabled: true, - vars: [{ required: true, name: 'var-name', type: 'text' }], - }, - ], - }, - ], - } as unknown) as PackageInfo; - - const validPackagePolicy: NewPackagePolicy = { - name: 'pkgPolicy1-1', - namespace: 'default', - policy_id: 'test-policy', - enabled: true, - output_id: 'test-output', - inputs: [ - { - type: 'foo', - enabled: true, - vars: { - 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' }, - 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' }, - 'foo-input3-var-name': { value: ['test'], type: 'text' }, - }, - streams: [ - { - data_stream: { dataset: 'foo', type: 'logs' }, - enabled: true, - vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, - }, - ], - }, - { - type: 'bar', - enabled: true, - vars: { - 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' }, - 'bar-input2-var-name': { value: 'test', type: 'text' }, - }, - streams: [ - { - data_stream: { dataset: 'bar', type: 'logs' }, - enabled: true, - vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, - }, - { - data_stream: { dataset: 'bar2', type: 'logs' }, - enabled: true, - vars: { 'var-name': { value: undefined, type: 'text' } }, - }, - ], - }, - { - type: 'with-no-config-or-streams', - enabled: true, - streams: [], - }, - { - type: 'with-disabled-streams', - enabled: true, - streams: [ - { - data_stream: { dataset: 'disabled', type: 'logs' }, - enabled: false, - vars: { 'var-name': { value: undefined, type: 'text' } }, - }, - { - data_stream: { dataset: 'disabled2', type: 'logs' }, - enabled: false, - }, - ], - }, - { - type: 'with-no-stream-vars', - enabled: true, - vars: { - 'var-name': { value: 'test', type: 'text' }, - }, - streams: [ - { - data_stream: { dataset: 'with-no-stream-vars-bar', type: 'logs' }, - enabled: true, - }, - ], - }, - ], - }; - - const invalidPackagePolicy: NewPackagePolicy = { - ...validPackagePolicy, - name: '', - inputs: [ - { - type: 'foo', - enabled: true, - vars: { - 'foo-input-var-name': { value: undefined, type: 'text' }, - 'foo-input2-var-name': { value: '', type: 'text' }, - 'foo-input3-var-name': { value: [], type: 'text' }, - }, - streams: [ - { - data_stream: { dataset: 'foo', type: 'logs' }, - enabled: true, - vars: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, - }, - ], - }, - { - type: 'bar', - enabled: true, - vars: { - 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' }, - 'bar-input2-var-name': { value: undefined, type: 'text' }, - }, - streams: [ - { - data_stream: { dataset: 'bar', type: 'logs' }, - enabled: true, - vars: { 'var-name': { value: ' \n\n', type: 'yaml' } }, - }, - { - data_stream: { dataset: 'bar2', type: 'logs' }, - enabled: true, - vars: { 'var-name': { value: undefined, type: 'text' } }, - }, - ], - }, - { - type: 'with-no-config-or-streams', - enabled: true, - streams: [], - }, - { - type: 'with-disabled-streams', - enabled: true, - streams: [ - { - data_stream: { dataset: 'disabled', type: 'logs' }, - enabled: false, - vars: { - 'var-name': { - value: 'invalid value but not checked due to not enabled', - type: 'text', - }, - }, - }, - { - data_stream: { dataset: 'disabled2', type: 'logs' }, - enabled: false, - }, - ], - }, - { - type: 'with-no-stream-vars', - enabled: true, - vars: { - 'var-name': { value: undefined, type: 'text' }, - }, - streams: [ - { - data_stream: { dataset: 'with-no-stream-vars-bar', type: 'logs' }, - enabled: true, - }, - ], - }, - ], - }; - - const noErrorsValidationResults = { - name: null, - description: null, - namespace: null, - inputs: { - foo: { - vars: { - 'foo-input-var-name': null, - 'foo-input2-var-name': null, - 'foo-input3-var-name': null, - }, - streams: { foo: { vars: { 'var-name': null } } }, - }, - bar: { - vars: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, - streams: { - bar: { vars: { 'var-name': null } }, - bar2: { vars: { 'var-name': null } }, - }, - }, - 'with-disabled-streams': { - streams: { - disabled: { - vars: { 'var-name': null }, - }, - disabled2: {}, - }, - }, - 'with-no-stream-vars': { - streams: { - 'with-no-stream-vars-bar': {}, - }, - vars: { 'var-name': null }, - }, - }, - }; - - it('returns no errors for valid package policy', () => { - expect(validatePackagePolicy(validPackagePolicy, mockPackage)).toEqual( - noErrorsValidationResults - ); - }); - - it('returns errors for invalid package policy', () => { - expect(validatePackagePolicy(invalidPackagePolicy, mockPackage)).toEqual({ - name: ['Name is required'], - description: null, - namespace: null, - inputs: { - foo: { - vars: { - 'foo-input-var-name': null, - 'foo-input2-var-name': ['foo-input2-var-name is required'], - 'foo-input3-var-name': ['foo-input3-var-name is required'], - }, - streams: { foo: { vars: { 'var-name': ['Invalid YAML format'] } } }, - }, - bar: { - vars: { - 'bar-input-var-name': ['Invalid format'], - 'bar-input2-var-name': ['bar-input2-var-name is required'], - }, - streams: { - bar: { vars: { 'var-name': ['var-name is required'] } }, - bar2: { vars: { 'var-name': null } }, - }, - }, - 'with-disabled-streams': { - streams: { - disabled: { vars: { 'var-name': null } }, - disabled2: {}, - }, - }, - 'with-no-stream-vars': { - vars: { - 'var-name': ['var-name is required'], - }, - streams: { 'with-no-stream-vars-bar': {} }, - }, - }, - }); - }); - - it('returns no errors for disabled inputs', () => { - const disabledInputs = invalidPackagePolicy.inputs.map((input) => ({ - ...input, - enabled: false, - })); - expect( - validatePackagePolicy({ ...validPackagePolicy, inputs: disabledInputs }, mockPackage) - ).toEqual(noErrorsValidationResults); - }); - - it('returns only package policy and input-level errors for disabled streams', () => { - const inputsWithDisabledStreams = invalidPackagePolicy.inputs.map((input) => - input.streams - ? { - ...input, - streams: input.streams.map((stream) => ({ ...stream, enabled: false })), - } - : input - ); - expect( - validatePackagePolicy( - { ...invalidPackagePolicy, inputs: inputsWithDisabledStreams }, - mockPackage - ) - ).toEqual({ - name: ['Name is required'], - description: null, - namespace: null, - inputs: { - foo: { - vars: { - 'foo-input-var-name': null, - 'foo-input2-var-name': ['foo-input2-var-name is required'], - 'foo-input3-var-name': ['foo-input3-var-name is required'], - }, - streams: { foo: { vars: { 'var-name': null } } }, - }, - bar: { - vars: { - 'bar-input-var-name': ['Invalid format'], - 'bar-input2-var-name': ['bar-input2-var-name is required'], - }, - streams: { - bar: { vars: { 'var-name': null } }, - bar2: { vars: { 'var-name': null } }, - }, - }, - 'with-disabled-streams': { - streams: { - disabled: { - vars: { 'var-name': null }, - }, - disabled2: {}, - }, - }, - 'with-no-stream-vars': { - vars: { - 'var-name': ['var-name is required'], - }, - streams: { 'with-no-stream-vars-bar': {} }, - }, - }, - }); - }); - - it('returns no errors for packages with no package policies', () => { - expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: undefined, - }) - ).toEqual({ - name: null, - description: null, - namespace: null, - inputs: null, - }); - expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: [], - }) - ).toEqual({ - name: null, - description: null, - namespace: null, - inputs: null, - }); - }); - - it('returns no errors for packages with no inputs', () => { - expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: [{} as RegistryPolicyTemplate], - }) - ).toEqual({ - name: null, - description: null, - namespace: null, - inputs: null, - }); - expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: [({ inputs: [] } as unknown) as RegistryPolicyTemplate], - }) - ).toEqual({ - name: null, - description: null, - namespace: null, - inputs: null, - }); - }); -}); - -describe('Fleet - validationHasErrors()', () => { - it('returns true for stream validation results with errors', () => { - expect( - validationHasErrors({ - vars: { foo: ['foo error'], bar: null }, - }) - ).toBe(true); - }); - - it('returns false for stream validation results with no errors', () => { - expect( - validationHasErrors({ - vars: { foo: null, bar: null }, - }) - ).toBe(false); - }); - - it('returns true for input validation results with errors', () => { - expect( - validationHasErrors({ - vars: { foo: ['foo error'], bar: null }, - streams: { stream1: { vars: { foo: null, bar: null } } }, - }) - ).toBe(true); - expect( - validationHasErrors({ - vars: { foo: null, bar: null }, - streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, - }) - ).toBe(true); - }); - - it('returns false for input validation results with no errors', () => { - expect( - validationHasErrors({ - vars: { foo: null, bar: null }, - streams: { stream1: { vars: { foo: null, bar: null } } }, - }) - ).toBe(false); - }); - - it('returns true for package policy validation results with errors', () => { - expect( - validationHasErrors({ - name: ['name error'], - description: null, - namespace: null, - inputs: { - input1: { - vars: { foo: null, bar: null }, - streams: { stream1: { vars: { foo: null, bar: null } } }, - }, - }, - }) - ).toBe(true); - expect( - validationHasErrors({ - name: null, - description: null, - namespace: null, - inputs: { - input1: { - vars: { foo: ['foo error'], bar: null }, - streams: { stream1: { vars: { foo: null, bar: null } } }, - }, - }, - }) - ).toBe(true); - expect( - validationHasErrors({ - name: null, - description: null, - namespace: null, - inputs: { - input1: { - vars: { foo: null, bar: null }, - streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, - }, - }, - }) - ).toBe(true); - }); - - it('returns false for package policy validation results with no errors', () => { - expect( - validationHasErrors({ - name: null, - description: null, - namespace: null, - inputs: { - input1: { - vars: { foo: null, bar: null }, - streams: { stream1: { vars: { foo: null, bar: null } } }, - }, - }, - }) - ).toBe(false); - }); -}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx index e64598e583d353..1ff5d20baec068 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiHorizontalRule, EuiFlexGroup, @@ -15,86 +15,92 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { - PackageInfo, - RegistryStream, - NewPackagePolicy, - NewPackagePolicyInput, -} from '../../../types'; +import type { PackageInfo, NewPackagePolicy, NewPackagePolicyInput } from '../../../types'; import { Loading } from '../../../components'; +import { getStreamsForInputType, doesPackageHaveIntegrations } from '../../../services'; import type { PackagePolicyValidationResults } from './services'; import { PackagePolicyInputPanel } from './components'; -const findStreamsForInputType = ( - inputType: string, - packageInfo: PackageInfo -): Array => { - const streams: Array = []; - - (packageInfo.data_streams || []).forEach((dataStream) => { - (dataStream.streams || []).forEach((stream) => { - if (stream.input === inputType) { - streams.push({ - ...stream, - data_stream: { - dataset: dataStream.dataset, - }, - }); - } - }); - }); - - return streams; -}; - export const StepConfigurePackagePolicy: React.FunctionComponent<{ packageInfo: PackageInfo; + showOnlyIntegration?: string; packagePolicy: NewPackagePolicy; updatePackagePolicy: (fields: Partial) => void; validationResults: PackagePolicyValidationResults; submitAttempted: boolean; -}> = ({ packageInfo, packagePolicy, updatePackagePolicy, validationResults, submitAttempted }) => { +}> = ({ + packageInfo, + showOnlyIntegration, + packagePolicy, + updatePackagePolicy, + validationResults, + submitAttempted, +}) => { + const hasIntegrations = useMemo(() => doesPackageHaveIntegrations(packageInfo), [packageInfo]); + const packagePolicyTemplates = useMemo( + () => + showOnlyIntegration + ? (packageInfo.policy_templates || []).filter( + (policyTemplate) => policyTemplate.name === showOnlyIntegration + ) + : packageInfo.policy_templates || [], + [packageInfo.policy_templates, showOnlyIntegration] + ); + // Configure inputs (and their streams) // Assume packages only export one config template for now const renderConfigureInputs = () => - packageInfo.policy_templates && - packageInfo.policy_templates[0] && - packageInfo.policy_templates[0].inputs && - packageInfo.policy_templates[0].inputs.length ? ( + packagePolicyTemplates.length ? ( <> - {packageInfo.policy_templates[0].inputs.map((packageInput) => { - const packagePolicyInput = packagePolicy.inputs.find( - (input) => input.type === packageInput.type - ); - const packageInputStreams = findStreamsForInputType(packageInput.type, packageInfo); - return packagePolicyInput ? ( - - ) => { - const indexOfUpdatedInput = packagePolicy.inputs.findIndex( - (input) => input.type === packageInput.type - ); - const newInputs = [...packagePolicy.inputs]; - newInputs[indexOfUpdatedInput] = { - ...newInputs[indexOfUpdatedInput], - ...updatedInput, - }; - updatePackagePolicy({ - inputs: newInputs, - }); - }} - inputValidationResults={validationResults!.inputs![packagePolicyInput.type]} - forceShowErrors={submitAttempted} - /> - - - ) : null; + {packagePolicyTemplates.map((policyTemplate) => { + return (policyTemplate.inputs || []).map((packageInput) => { + const packagePolicyInput = packagePolicy.inputs.find( + (input) => + input.type === packageInput.type && + (hasIntegrations ? input.policy_template === policyTemplate.name : true) + ); + const packageInputStreams = getStreamsForInputType( + packageInput.type, + packageInfo, + hasIntegrations ? policyTemplate.data_streams : [] + ); + return packagePolicyInput ? ( + + ) => { + const indexOfUpdatedInput = packagePolicy.inputs.findIndex( + (input) => + input.type === packageInput.type && + (hasIntegrations ? input.policy_template === policyTemplate.name : true) + ); + const newInputs = [...packagePolicy.inputs]; + newInputs[indexOfUpdatedInput] = { + ...newInputs[indexOfUpdatedInput], + ...updatedInput, + }; + updatePackagePolicy({ + inputs: newInputs, + }); + }} + inputValidationResults={ + validationResults!.inputs![ + hasIntegrations + ? `${policyTemplate.name}-${packagePolicyInput.type}` + : packagePolicyInput.type + ] + } + forceShowErrors={submitAttempted} + /> + + + ) : null; + }); })} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index 380e49a1d8dd97..7444bed6ed3fdb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -38,7 +38,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ agentPolicy: AgentPolicy; packageInfo: PackageInfo; packagePolicy: NewPackagePolicy; - integration?: string; + integrationToEnable?: string; updatePackagePolicy: (fields: Partial) => void; validationResults: PackagePolicyValidationResults; submitAttempted: boolean; @@ -47,7 +47,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ agentPolicy, packageInfo, packagePolicy, - integration, + integrationToEnable, updatePackagePolicy, validationResults, submitAttempted, @@ -95,7 +95,8 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 : 1 }`, - packagePolicy.description + packagePolicy.description, + integrationToEnable ) ); } @@ -107,7 +108,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ namespace: agentPolicy.namespace, }); } - }, [packagePolicy, agentPolicy, packageInfo, updatePackagePolicy, integration]); + }, [packagePolicy, agentPolicy, packageInfo, updatePackagePolicy, integrationToEnable]); return validationResults ? ( = ({ agentPolicy, ...rest }) => { - const { getHref } = useLink(); const { application } = useStartServices(); const hasWriteCapabilities = useCapabilities().write; - const refreshAgentPolicy = useAgentPolicyRefresh(); // With the package policies provided on input, generate the list of package policies // used in the InMemoryTable (flattens some values for search) as well as @@ -171,71 +166,15 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ actions: [ { render: (packagePolicy: InMemoryPackagePolicy) => { - const menuItems = [ - // FIXME: implement View package policy action - // {}} - // key="packagePolicyView" - // > - // - // , - - - , - // FIXME: implement Copy package policy action - // {}} key="packagePolicyCopy"> - // - // , - ]; - - if (!agentPolicy.is_managed) { - menuItems.push( - - {(deletePackagePoliciesPrompt) => { - return ( - { - deletePackagePoliciesPrompt([packagePolicy.id], refreshAgentPolicy); - }} - > - - - ); - }} - - ); - } - return ; + return ( + + ); }, }, ], }, ], - [agentPolicy, getHref, hasWriteCapabilities, refreshAgentPolicy] + [agentPolicy] ); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index e91d32a78ec963..1ea1a7de53b959 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -20,13 +20,13 @@ import { sendUpdateAgentPolicy, useConfig, sendGetAgentStatus, + useAgentPolicyRefresh, } from '../../../../../hooks'; import { AgentPolicyForm, agentPolicyFormValidation, ConfirmDeployAgentPolicyModal, } from '../../../components'; -import { useAgentPolicyRefresh } from '../../hooks'; const FormWrapper = styled.div` max-width: 800px; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/hooks/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/hooks/index.ts index 07daef8bb2ced8..fb269f95213d9b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/hooks/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/hooks/index.ts @@ -6,4 +6,3 @@ */ export { useGetAgentStatus, AgentStatusRefreshContext } from './use_agent_status'; -export { AgentPolicyRefreshContext, useAgentPolicyRefresh } from './use_config'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 7138abb081a77f..9d3fe64b6639d3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -28,6 +28,7 @@ import styled from 'styled-components'; import type { AgentPolicy, AgentPolicyDetailsDeployAgentAction } from '../../../types'; import { FLEET_ROUTING_PATHS } from '../../../constants'; import { + AgentPolicyRefreshContext, useGetOneAgentPolicy, useLink, useBreadcrumbs, @@ -39,7 +40,7 @@ import { Loading, Error } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { LinkedAgentCount, AgentPolicyActionMenu } from '../components'; -import { AgentPolicyRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; +import { useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { PackagePoliciesView, SettingsView } from './components'; const Divider = styled.div` diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx index 540b06d7a9786b..fb80611e5295c8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -23,8 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { Agent, AgentPolicy } from '../../../../../types'; import { useKibanaVersion } from '../../../../../hooks'; import { isAgentUpgradeable } from '../../../../../services'; -import { AgentPolicyPackageBadges } from '../../../components/agent_policy_package_badges'; -import { AgentPolicySummaryLine } from '../../../../../components'; +import { AgentPolicyPackageBadges, AgentPolicySummaryLine } from '../../../../../components'; // Allows child text to be truncated const FlexItemWithMinWidth = styled(EuiFlexItem)` diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 70cb6cddad5fad..672b8718c9cbe7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -22,7 +22,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; -import { AgentEnrollmentFlyout } from '../components'; import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { usePagination, @@ -37,7 +36,11 @@ import { useKibanaVersion, useStartServices, } from '../../../hooks'; -import { AgentPolicySummaryLine, ContextMenuActions } from '../../../components'; +import { + AgentEnrollmentFlyout, + AgentPolicySummaryLine, + ContextMenuActions, +} from '../../../components'; import { AgentStatusKueryHelper, isAgentUpgradeable } from '../../../services'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx index 83f09789a94148..20d366b01af2bb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx @@ -26,10 +26,15 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DownloadStep } from '../components/agent_enrollment_flyout/steps'; -import { useStartServices, useGetOutputs, sendGenerateServiceToken } from '../../../hooks'; -import { PLATFORM_OPTIONS, usePlatform } from '../hooks/use_platform'; -import type { PLATFORM_TYPE } from '../hooks/use_platform'; +import { DownloadStep } from '../../../components'; +import { + useStartServices, + useGetOutputs, + sendGenerateServiceToken, + usePlatform, + PLATFORM_OPTIONS, +} from '../../../hooks'; +import type { PLATFORM_TYPE } from '../../../hooks'; const FlexItemWithMinWidth = styled(EuiFlexItem)` min-width: 0px; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx index 8ee401d3c4ddf7..f15b58200ea889 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx @@ -24,7 +24,7 @@ import { useStartServices, useGetAgentPolicies, } from '../../../../hooks'; -import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; +import { AgentPolicyPackageBadges } from '../../../../components'; interface Props { onClose: () => void; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/index.tsx index 45f09c79d55337..966a7779413735 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/index.tsx @@ -7,7 +7,6 @@ export * from './loading'; export * from './agent_reassign_policy_modal'; -export * from './agent_enrollment_flyout'; export * from './agent_health'; export * from './agent_unenroll_modal'; export * from './agent_upgrade_modal'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx index 40642587c1a38f..67758282521b79 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/list_layout.tsx @@ -14,7 +14,7 @@ import { useRouteMatch } from 'react-router-dom'; import { FLEET_ROUTING_PATHS } from '../../../constants'; import { WithHeaderLayout } from '../../../layouts'; import { useCapabilities, useLink, useGetAgentPolicies } from '../../../hooks'; -import { AgentEnrollmentFlyout } from '../components'; +import { AgentEnrollmentFlyout } from '../../../components'; export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { const { getHref } = useLink(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx index 66e0c338dbbbcd..8dc9ad33962e0d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx @@ -22,6 +22,7 @@ import { import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { ENROLLMENT_API_KEYS_INDEX } from '../../../constants'; +import { NewEnrollmentTokenModal } from '../../../components'; import { useBreadcrumbs, usePagination, @@ -34,7 +35,6 @@ import { import type { EnrollmentAPIKey, GetAgentPoliciesResponseItem } from '../../../types'; import { SearchBar } from '../../../components/search_bar'; -import { NewEnrollmentTokenModal } from './components/new_enrollment_key_modal'; import { ConfirmEnrollmentTokenDelete } from './components/confirm_delete_modal'; const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId }) => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/index.tsx index 9da10c1de2be06..f905fd1c89da27 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/overview/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/overview/index.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; import { useGetAgentPolicies, useBreadcrumbs } from '../../hooks'; -import { AgentEnrollmentFlyout } from '../agents/components'; +import { AgentEnrollmentFlyout } from '../../components'; import { OverviewAgentSection } from './components/agent_section'; import { OverviewPolicySection } from './components/agent_policy_section'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 4063bc6371cbdc..04253994d3875e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -109,9 +109,7 @@ describe('when on integration detail', () => { pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' })[1] ); }); - expect(testRenderer.history.location.pathname).toEqual( - '/app/integrations/detail/nginx-0.3.7/overview' - ); + expect(testRenderer.history.location.pathname).toEqual('/detail/nginx-0.3.7/overview'); }); }); @@ -204,12 +202,30 @@ describe('when on integration detail', () => { expect(firstRowAgentCount.tagName).not.toEqual('A'); }); + it('should show add agent button if agent count is zero', async () => { + await mockedApi.waitForApi(); + const firstRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[0]; + expect(firstRowAgentCount.textContent).toEqual('0'); + + const addAgentButton = renderResult.getAllByTestId('addAgentButton')[0]; + expect(addAgentButton).not.toBeNull(); + }); + it('should show link for agent count if greater than zero', async () => { await mockedApi.waitForApi(); const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; expect(secondRowAgentCount.textContent).toEqual('100'); expect(secondRowAgentCount.tagName).toEqual('A'); }); + + it('should NOT show add agent button if agent count is greater than zero', async () => { + await mockedApi.waitForApi(); + const secondRowAgentCount = renderResult.getAllByTestId('rowAgentCount')[1]; + expect(secondRowAgentCount.textContent).toEqual('100'); + + const addAgentButton = renderResult.getAllByTestId('addAgentButton')[1]; + expect(addAgentButton).toBeUndefined(); + }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index d5dce6334762c0..7da7328fdebbca 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -5,23 +5,44 @@ * 2.0. */ -import React, { memo, useCallback, useMemo } from 'react'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import { Redirect } from 'react-router-dom'; import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui'; -import { EuiBasicTable, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiBasicTable, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; +import styled from 'styled-components'; import { InstallStatus } from '../../../../../types'; -import { useLink, useUrlPagination, useGetPackageInstallStatus } from '../../../../../hooks'; +import { + useLink, + useUrlPagination, + useGetPackageInstallStatus, + AgentPolicyRefreshContext, +} from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; -import { AgentPolicySummaryLine } from '../../../../../components'; -import { LinkedAgentCount } from '../../../../../components'; +import { + AgentEnrollmentFlyout, + AgentPolicySummaryLine, + LinkedAgentCount, + PackagePolicyActionsMenu, +} from '../../../../../components'; import type { PackagePolicyAndAgentPolicy } from './use_package_policies_with_agent_policy'; import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; import { Persona } from './persona'; +const AddAgentButton = styled(EuiButtonIcon)` + margin-left: ${(props) => props.theme.eui.euiSizeS}; +`; + const IntegrationDetailsLink = memo<{ packagePolicy: PackagePolicyAndAgentPolicy['packagePolicy']; }>(({ packagePolicy }) => { @@ -39,16 +60,18 @@ const IntegrationDetailsLink = memo<{ ); }); + interface PackagePoliciesPanelProps { name: string; version: string; } export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(null); const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); - const { data, isLoading } = usePackagePoliciesWithAgentPolicy({ + const { data, isLoading, resendRequest: refreshPolicies } = usePackagePoliciesWithAgentPolicy({ page: pagination.currentPage, perPage: pagination.pageSize, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`, @@ -97,16 +120,36 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps defaultMessage: 'Agents', }), truncateText: true, - align: 'right', + align: 'left', width: '8ch', render({ packagePolicy, agentPolicy }: PackagePolicyAndAgentPolicy) { + const count = agentPolicy?.agents ?? 0; + return ( - + <> + + {count === 0 && ( + + setFlyoutOpenForPolicyId(agentPolicy.id)} + data-test-subj="addAgentButton" + /> + + )} + ); }, }, @@ -134,6 +177,19 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); }, }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { + defaultMessage: 'Actions', + }), + width: '8ch', + align: 'right', + render({ agentPolicy, packagePolicy }) { + return ( + + ); + }, + }, ], [] ); @@ -165,19 +221,31 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps } return ( - - - - + + + + + + + {flyoutOpenForPolicyId && ( + setFlyoutOpenForPolicyId(null)} + agentPolicies={ + data?.items + .filter(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) + .map(({ agentPolicy }) => agentPolicy) ?? [] + } /> - - + )} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts index aae43a1acd5689..33c1d3ff773023 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/use_package_policies_with_agent_policy.ts @@ -43,11 +43,13 @@ export const usePackagePoliciesWithAgentPolicy = ( isLoading: boolean; error: Error | null; data?: GetPackagePoliciesWithAgentPolicy; + resendRequest: () => void; } => { const { data: packagePoliciesData, error, isLoading: isLoadingPackagePolicies, + resendRequest, } = useGetPackagePolicies(query); const agentPoliciesFilter = useMemo(() => { @@ -124,5 +126,6 @@ export const usePackagePoliciesWithAgentPolicy = ( data: enrichedData, error, isLoading: isLoadingPackagePolicies || isLoadingAgentPolicies, + resendRequest, }; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx similarity index 98% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx rename to x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx index 4edc1121b1091a..dc239213baf360 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiButton, EuiCallOut, EuiSelect, EuiSpacer, EuiText } from '@elastic/eui'; -import { SO_SEARCH_LIMIT } from '../../../../constants'; -import type { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../../../types'; +import { SO_SEARCH_LIMIT } from '../../constants'; +import type { AgentPolicy, GetEnrollmentAPIKeysResponse } from '../../types'; import { sendGetEnrollmentAPIKeys, useStartServices, sendCreateEnrollmentAPIKey, -} from '../../../../hooks'; +} from '../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; type Props = { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx similarity index 95% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx rename to x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx index 1aa88dcef4adc1..84f881e8baa0cc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/index.tsx @@ -26,8 +26,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useGetSettings, useUrlModal } from '../../../../hooks'; -import type { AgentPolicy } from '../../../../types'; +import { useGetSettings, useUrlModal } from '../../hooks'; +import type { AgentPolicy } from '../../types'; import { ManagedInstructions } from './managed_instructions'; import { StandaloneInstructions } from './standalone_instructions'; @@ -37,6 +37,11 @@ interface Props { agentPolicies?: AgentPolicy[]; } +export * from './agent_policy_selection'; +export * from './managed_instructions'; +export * from './standalone_instructions'; +export * from './steps'; + const MissingFleetServerHostCallout: React.FunctionComponent = () => { const { setModal } = useUrlModal(); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx similarity index 91% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx rename to x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index df1630abfab476..5f2e0d6e4c4145 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -11,22 +11,17 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { AgentPolicy } from '../../../../types'; -import { - useGetOneEnrollmentAPIKey, - useGetSettings, - useLink, - useFleetStatus, -} from '../../../../hooks'; -import { NewEnrollmentTokenModal } from '../../enrollment_token_list_page/components/new_enrollment_key_modal'; +import type { AgentPolicy } from '../../types'; +import { NewEnrollmentTokenModal } from '../../components'; +import { useGetOneEnrollmentAPIKey, useGetSettings, useLink, useFleetStatus } from '../../hooks'; -import { ManualInstructions } from '../../../../components/enrollment_instructions'; +import { ManualInstructions } from '../../components/enrollment_instructions'; import { FleetServerRequirementPage, ServiceTokenStep, FleetServerCommandStep, useFleetServerInstructions, -} from '../../agent_requirements_page'; +} from '../../applications/fleet/sections/agents/agent_requirements_page'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx similarity index 98% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx rename to x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index 1d830b2c578b10..0fd846b074f98c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -22,9 +22,9 @@ import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/st import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { AgentPolicy } from '../../../../types'; -import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../../../hooks'; -import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../../services'; +import type { AgentPolicy } from '../../types'; +import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../hooks'; +import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx similarity index 94% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx rename to x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index 8ba0098b3d2778..fc22cbdd047c3b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -10,9 +10,9 @@ import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import type { AgentPolicy, PackagePolicy } from '../../../../types'; -import { sendGetOneAgentPolicy } from '../../../../hooks'; -import { FLEET_SERVER_PACKAGE } from '../../../../constants'; +import type { AgentPolicy, PackagePolicy } from '../../types'; +import { sendGetOneAgentPolicy } from '../../hooks'; +import { FLEET_SERVER_PACKAGE } from '../../constants'; import { EnrollmentStepAgentPolicy } from './agent_policy_selection'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx b/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx similarity index 94% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx rename to x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx index 605888349ab4ec..418e274022461b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_package_badges.tsx +++ b/x-pack/plugins/fleet/public/components/agent_policy_package_badges.tsx @@ -10,11 +10,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, EuiCallOut } from '@elastic/eui'; -import { FLEET_SERVER_PACKAGE } from '../../../../../../common/constants'; +import { FLEET_SERVER_PACKAGE } from '../../common/constants'; -import type { PackagePolicy, PackagePolicyPackage } from '../../../types'; -import { useGetOneAgentPolicy } from '../../../hooks'; -import { PackageIcon } from '../../../components'; +import type { PackagePolicy, PackagePolicyPackage } from '../types'; +import { useGetOneAgentPolicy } from '../hooks'; +import { PackageIcon } from '../components'; interface Props { agentPolicyId: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/danger_eui_context_menu_item.tsx b/x-pack/plugins/fleet/public/components/danger_eui_context_menu_item.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/danger_eui_context_menu_item.tsx rename to x-pack/plugins/fleet/public/components/danger_eui_context_menu_item.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/index.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/index.tsx rename to x-pack/plugins/fleet/public/components/enrollment_instructions/index.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx similarity index 95% rename from x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx rename to x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index c9f011c59504be..f746a1c9144607 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -12,8 +12,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import type { EnrollmentAPIKey } from '../../../types'; -import { PLATFORM_OPTIONS, usePlatform } from '../../../sections/agents/hooks/use_platform'; -import type { PLATFORM_TYPE } from '../../../sections/agents/hooks/use_platform'; +import { PLATFORM_OPTIONS, usePlatform } from '../../../hooks'; +import type { PLATFORM_TYPE } from '../../../hooks'; interface Props { fleetServerHosts: string[]; diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 6db18c0e73a141..d054b96ccb4c62 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -13,5 +13,11 @@ export { ExtensionWrapper } from './extension_wrapper'; export { AlphaMessaging } from './alpha_messaging'; export { AlphaFlyout } from './alpha_flyout'; export { HeaderProps, Header } from './header'; +export { NewEnrollmentTokenModal } from './new_enrollment_key_modal'; +export { AgentPolicyPackageBadges } from './agent_policy_package_badges'; +export { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; +export { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; +export { PackagePolicyActionsMenu } from './package_policy_actions_menu'; export * from './link_and_revision'; export * from './settings_flyout'; +export * from './agent_enrollment_flyout'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_modal.tsx b/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx similarity index 97% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_modal.tsx rename to x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx index 29e130f5583ab5..0752c1ab348893 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_modal.tsx +++ b/x-pack/plugins/fleet/public/components/new_enrollment_key_modal.tsx @@ -9,8 +9,8 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, EuiForm, EuiFormRow, EuiFieldText, EuiSelect } from '@elastic/eui'; -import type { AgentPolicy, EnrollmentAPIKey } from '../../../../types'; -import { useInput, useStartServices, sendCreateEnrollmentAPIKey } from '../../../../hooks'; +import type { AgentPolicy, EnrollmentAPIKey } from '../types'; +import { useInput, useStartServices, sendCreateEnrollmentAPIKey } from '../hooks'; function useCreateApiKeyForm( policyIdDefaultValue: string | undefined, diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx new file mode 100644 index 00000000000000..03bf2095f7f3e0 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -0,0 +1,112 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { AgentPolicy, PackagePolicy } from '../types'; + +import { useAgentPolicyRefresh, useCapabilities, useLink } from '../hooks'; + +import { AgentEnrollmentFlyout } from './agent_enrollment_flyout'; +import { ContextMenuActions } from './context_menu_actions'; +import { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; +import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; + +export const PackagePolicyActionsMenu: React.FunctionComponent<{ + agentPolicy: AgentPolicy; + packagePolicy: PackagePolicy; +}> = ({ agentPolicy, packagePolicy }) => { + const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); + const { getHref } = useLink(); + const hasWriteCapabilities = useCapabilities().write; + const refreshAgentPolicy = useAgentPolicyRefresh(); + + const onEnrollmentFlyoutClose = useMemo(() => { + return () => setIsEnrollmentFlyoutOpen(false); + }, []); + + const menuItems = [ + // FIXME: implement View package policy action + // {}} + // key="packagePolicyView" + // > + // + // , + setIsEnrollmentFlyoutOpen(true)} + key="addAgent" + > + + , + + + , + // FIXME: implement Copy package policy action + // {}} key="packagePolicyCopy"> + // + // , + ]; + + if (!agentPolicy.is_managed) { + menuItems.push( + + {(deletePackagePoliciesPrompt) => { + return ( + { + deletePackagePoliciesPrompt([packagePolicy.id], refreshAgentPolicy); + }} + > + + + ); + }} + + ); + } + return ( + <> + {isEnrollmentFlyoutOpen && ( + + + + )} + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx similarity index 98% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx rename to x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx index 198886e9b9c7ff..86f064405497b7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx @@ -10,9 +10,9 @@ import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useStartServices, sendRequest, sendDeletePackagePolicy, useConfig } from '../../../hooks'; -import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; -import type { AgentPolicy } from '../../../types'; +import { useStartServices, sendRequest, sendDeletePackagePolicy, useConfig } from '../hooks'; +import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../common/constants'; +import type { AgentPolicy } from '../types'; interface Props { agentPolicy: AgentPolicy; diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index 0b413f72094d9d..9f41e5c7cc92b5 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -24,3 +24,5 @@ export * from './use_url_params'; export * from './use_fleet_status'; export * from './use_ui_extension'; export * from './use_intra_app_state'; +export * from './use_platform'; +export * from './use_agent_policy_refresh'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/hooks/use_config.tsx b/x-pack/plugins/fleet/public/hooks/use_agent_policy_refresh.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/hooks/use_config.tsx rename to x-pack/plugins/fleet/public/hooks/use_agent_policy_refresh.tsx diff --git a/x-pack/plugins/fleet/public/hooks/use_link.ts b/x-pack/plugins/fleet/public/hooks/use_link.ts index df6f45af31d56b..6917e0f5c3b8e7 100644 --- a/x-pack/plugins/fleet/public/hooks/use_link.ts +++ b/x-pack/plugins/fleet/public/hooks/use_link.ts @@ -21,9 +21,7 @@ export const useLink = () => { const core = useStartServices(); return { getPath: (page: StaticPage | DynamicPage, values: DynamicPagePathValues = {}): string => { - const [basePath, path] = getSeparatePaths(page, values); - - return `${basePath}${path}`; + return getSeparatePaths(page, values)[1]; }, getAssetsPath: (path: string) => core.http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_platform.tsx b/x-pack/plugins/fleet/public/hooks/use_platform.tsx similarity index 100% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_platform.tsx rename to x-pack/plugins/fleet/public/hooks/use_platform.tsx diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 4575efe9637056..fbd3bddde744b8 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -30,6 +30,14 @@ export { LicenseService, isAgentUpgradeable, doesPackageHaveIntegrations, + PackagePolicyValidationResults, + PackagePolicyConfigValidationResults, + PackagePolicyInputValidationResults, + validatePackagePolicy, + validatePackagePolicyConfig, + validationHasErrors, + countValidationErrors, + getStreamsForInputType, } from '../../common'; export * from './pkg_key_from_package_info'; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 10e5a6ac57f576..bd7bb98eb7c07c 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -235,6 +235,7 @@ const getSavedObjectTypes = ( enabled: false, properties: { type: { type: 'keyword' }, + policy_template: { type: 'keyword' }, enabled: { type: 'boolean' }, vars: { type: 'flattened' }, config: { type: 'flattened' }, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 62b4578ab87b22..2a6036d99281e8 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -47,6 +47,10 @@ import type { Output, } from '../../common'; import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError } from '../errors'; +import { + storedPackagePoliciesToAgentPermissions, + DEFAULT_PERMISSIONS, +} from '../services/package_policies_to_agent_permissions'; import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; @@ -745,30 +749,49 @@ class AgentPolicyService { }), }; + const permissions = (await storedPackagePoliciesToAgentPermissions( + soClient, + agentPolicy.package_policies + )) || { _fallback: DEFAULT_PERMISSIONS }; + + permissions._elastic_agent_checks = { + cluster: DEFAULT_PERMISSIONS.cluster, + }; + + // TODO fetch this from the elastic agent package + const monitoringOutput = fullAgentPolicy.agent?.monitoring.use_output; + const monitoringNamespace = fullAgentPolicy.agent?.monitoring.namespace; + if ( + fullAgentPolicy.agent?.monitoring.enabled && + monitoringNamespace && + monitoringOutput && + fullAgentPolicy.outputs[monitoringOutput]?.type === 'elasticsearch' + ) { + const names: string[] = []; + if (fullAgentPolicy.agent.monitoring.logs) { + names.push(`logs-elastic_agent.*-${monitoringNamespace}`); + } + if (fullAgentPolicy.agent.monitoring.metrics) { + names.push(`metrics-elastic_agent.*-${monitoringNamespace}`); + } + + permissions._elastic_agent_checks.indices = [ + { + names, + privileges: ['auto_configure', 'create_doc'], + }, + ]; + } + // Only add permissions if output.type is "elasticsearch" fullAgentPolicy.output_permissions = Object.keys(fullAgentPolicy.outputs).reduce< NonNullable - >((permissions, outputName) => { + >((outputPermissions, outputName) => { const output = fullAgentPolicy.outputs[outputName]; if (output && output.type === 'elasticsearch') { - permissions[outputName] = {}; - permissions[outputName]._fallback = { - cluster: ['monitor'], - indices: [ - { - names: [ - 'logs-*', - 'metrics-*', - 'traces-*', - '.logs-endpoint.diagnostic.collection-*', - 'synthetics-*', - ], - privileges: ['auto_configure', 'create_doc'], - }, - ], - }; + outputPermissions[outputName] = permissions; } - return permissions; + return outputPermissions; }, {}); // only add settings if not in standalone diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index 2db6009270a3b6..dde6459addcbc5 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -224,24 +224,20 @@ export const getEsPackage = async ( ); const dataStreamManifest = safeLoad(soResDataStreamManifest.attributes.data_utf8); const { - title: dataStreamTitle, - release, ingest_pipeline: ingestPipeline, - type, dataset, streams: manifestStreams, + ...dataStreamManifestProps } = dataStreamManifest; const streams = parseAndVerifyStreams(manifestStreams, dataStreamPath); dataStreams.push({ dataset: dataset || `${pkgName}.${dataStreamPath}`, - title: dataStreamTitle, - release, package: pkgName, ingest_pipeline: ingestPipeline || 'default', path: dataStreamPath, - type, streams, + ...dataStreamManifestProps, }); }) ); diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts new file mode 100644 index 00000000000000..39759a6fc9e9c7 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -0,0 +1,341 @@ +/* + * 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. + */ + +jest.mock('./epm/packages'); +import type { SavedObjectsClientContract } from 'kibana/server'; + +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import type { PackagePolicy, RegistryDataStream } from '../types'; + +import { getPackageInfo } from './epm/packages'; +import { + getDataStreamPermissions, + storedPackagePoliciesToAgentPermissions, +} from './package_policies_to_agent_permissions'; + +const getPackageInfoMock = getPackageInfo as jest.MockedFunction; + +describe('storedPackagePoliciesToAgentPermissions()', () => { + let soClient: jest.Mocked; + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + }); + + it('Returns `undefined` if there are no package policies', async () => { + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, []); + expect(permissions).toBeUndefined(); + }); + + it('Returns the default permissions for string package policies', async () => { + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, ['foo']); + expect(permissions).toMatchObject({ + _fallback: { + cluster: ['monitor'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + 'synthetics-*', + '.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); + + it('Returns the default permissions if a package policy does not have a package', async () => { + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, [ + { name: 'foo', package: undefined } as PackagePolicy, + ]); + + expect(permissions).toMatchObject({ + foo: { + cluster: ['monitor'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + 'synthetics-*', + '.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); + + it('Returns the permissions for the enabled inputs', async () => { + getPackageInfoMock.mockResolvedValueOnce({ + name: 'test-package', + version: '0.0.0', + latestVersion: '0.0.0', + release: 'experimental', + format_version: '1.0.0', + title: 'Test Package', + description: '', + icons: [], + owner: { github: '' }, + status: 'not_installed', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + }, + }, + data_streams: [ + { + type: 'logs', + dataset: 'some-logs', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-logs', title: 'Test Logs', template_path: '' }], + }, + { + type: 'metrics', + dataset: 'some-metrics', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-metrics', title: 'Test Logs', template_path: '' }], + }, + ], + }); + + const packagePolicies: PackagePolicy[] = [ + { + id: '12345', + name: 'test-policy', + namespace: 'test', + enabled: true, + package: { name: 'test-package', version: '0.0.0', title: 'Test Package' }, + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + }, + ], + }, + { + type: 'test-metrics', + enabled: false, + streams: [ + { + id: 'test-logs', + enabled: false, + data_stream: { type: 'metrics', dataset: 'some-metrics' }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + output_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); + expect(permissions).toMatchObject({ + 'test-policy': { + indices: [ + { + names: ['logs-some-logs-test'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); + + it('Returns the dataset for the compiled data_streams', async () => { + getPackageInfoMock.mockResolvedValueOnce({ + name: 'test-package', + version: '0.0.0', + latestVersion: '0.0.0', + release: 'experimental', + format_version: '1.0.0', + title: 'Test Package', + description: '', + icons: [], + owner: { github: '' }, + status: 'not_installed', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + }, + }, + data_streams: [ + { + type: 'logs', + dataset: 'some-logs', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-logs', title: 'Test Logs', template_path: '' }], + }, + ], + }); + + const packagePolicies: PackagePolicy[] = [ + { + id: '12345', + name: 'test-policy', + namespace: 'test', + enabled: true, + package: { name: 'test-package', version: '0.0.0', title: 'Test Package' }, + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + compiled_stream: { data_stream: { dataset: 'compiled' } }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + output_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); + expect(permissions).toMatchObject({ + 'test-policy': { + indices: [ + { + names: ['logs-compiled-test'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); +}); + +describe('getDataStreamPermissions()', () => { + it('returns defaults for a datastream with no permissions', () => { + const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream); + + expect(permissions).toMatchObject({ + names: ['logs-test-*'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('adds the namespace to the index name', () => { + const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['logs-test-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('appends a wildcard if dataset is prefix', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + dataset_is_prefix: true, + } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['logs-test.*-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('prepends a dot if datastream is hidden', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + hidden: true, + } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['.logs-test-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('uses custom permissions if they are present in the datastream', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + permissions: { indices: ['read', 'write'] }, + } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['logs-test-namespace'], + privileges: ['read', 'write'], + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts new file mode 100644 index 00000000000000..bd73b88e7c893b --- /dev/null +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { SavedObjectsClientContract } from 'kibana/server'; + +import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPermissions } from '../../common'; +import { getPackageInfo } from '../../server/services/epm/packages'; + +import type { PackagePolicy } from '../types'; + +export const DEFAULT_PERMISSIONS = { + cluster: ['monitor'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + 'synthetics-*', + '.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], +}; + +export async function storedPackagePoliciesToAgentPermissions( + soClient: SavedObjectsClientContract, + packagePolicies: string[] | PackagePolicy[] +): Promise { + if (packagePolicies.length === 0) { + return; + } + + // I'm not sure what permissions to return for this case, so let's return the defaults + if (typeof packagePolicies[0] === 'string') { + return { _fallback: DEFAULT_PERMISSIONS }; + } + + const permissionEntries = (packagePolicies as PackagePolicy[]).map>( + async (packagePolicy) => { + if (!packagePolicy.package) { + return [packagePolicy.name, DEFAULT_PERMISSIONS]; + } + + const pkg = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + }); + + if (!pkg.data_streams || pkg.data_streams.length === 0) { + return [packagePolicy.name, undefined]; + } + + let dataStreamsForPermissions: DataStreamMeta[]; + + switch (pkg.name) { + case 'endpoint': + // - Endpoint doesn't store the `data_stream` metadata in + // `packagePolicy.inputs`, so we will use _all_ data_streams from the + // package. + dataStreamsForPermissions = pkg.data_streams; + break; + + case 'apm': + // - APM doesn't store the `data_stream` metadata in + // `packagePolicy.inputs`, so we will use _all_ data_streams from + // the package. + dataStreamsForPermissions = pkg.data_streams; + break; + + default: + // - Normal packages store some of the `data_stream` metadata in + // `packagePolicy.inputs[].streams[].data_stream` + // - The rest of the metadata needs to be fetched from the + // `data_stream` object in the package. The link is + // `packagePolicy.inputs[].type == pkg.data_streams.streams[].input` + // - Some packages (custom logs) have a compiled dataset, stored in + // `input.streams.compiled_stream.data_stream.dataset` + dataStreamsForPermissions = packagePolicy.inputs + .filter((i) => i.enabled) + .flatMap((input) => { + if (!input.streams) { + return []; + } + + const dataStreams_: DataStreamMeta[] = []; + + input.streams + .filter((s) => s.enabled) + .forEach((stream) => { + if (!('data_stream' in stream)) { + return; + } + + const ds = { + type: stream.data_stream.type, + dataset: + stream.compiled_stream?.data_stream?.dataset ?? stream.data_stream.dataset, + }; + + dataStreams_.push(ds); + }); + + return dataStreams_; + }); + } + + return [ + packagePolicy.name, + { + indices: dataStreamsForPermissions.map((ds) => + getDataStreamPermissions(ds, packagePolicy.namespace) + ), + }, + ]; + } + ); + + return Object.fromEntries(await Promise.all(permissionEntries)); +} + +interface DataStreamMeta { + type: string; + dataset: string; + dataset_is_prefix?: boolean; + hidden?: boolean; + permissions?: RegistryDataStreamPermissions; +} + +export function getDataStreamPermissions(dataStream: DataStreamMeta, namespace: string = '*') { + let index = `${dataStream.type}-${dataStream.dataset}`; + + if (dataStream.dataset_is_prefix) { + index = `${index}.*`; + } + + if (dataStream.hidden) { + index = `.${index}`; + } + + index += `-${namespace}`; + + return { + names: [index], + privileges: dataStream.permissions?.indices || ['auto_configure', 'create_doc'], + }; +} diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 46e5db3a958646..a6958ba88449a0 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -408,6 +408,11 @@ describe('Package policy service', () => { ], policy_templates: [ { + name: 'template_1', + inputs: [{ type: 'log', template_path: 'some_template_path.yml' }], + }, + { + name: 'template_2', inputs: [{ type: 'log', template_path: 'some_template_path.yml' }], }, ], @@ -416,6 +421,7 @@ describe('Package policy service', () => { [ { type: 'log', + policy_template: 'template_1', enabled: true, vars: { hosts: { @@ -433,12 +439,24 @@ describe('Package policy service', () => { }, ], }, + { + type: 'log', + policy_template: 'template_2', + enabled: true, + vars: { + hosts: { + value: ['localhost'], + }, + }, + streams: [], + }, ] ); expect(inputs).toEqual([ { type: 'log', + policy_template: 'template_1', enabled: true, vars: { hosts: { @@ -465,6 +483,20 @@ describe('Package policy service', () => { }, ], }, + { + type: 'log', + policy_template: 'template_2', + enabled: true, + vars: { + hosts: { + value: ['localhost'], + }, + }, + compiled_input: { + hosts: ['localhost'], + }, + streams: [], + }, ]); }); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 02f9731421ba04..93bcef458279c0 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -511,11 +511,17 @@ async function _compilePackagePolicyInput( vars: PackagePolicy['vars'], input: PackagePolicyInput ) { - if ((!input.enabled || !pkgInfo.policy_templates?.[0]?.inputs?.length) ?? 0 > 0) { + const packagePolicyTemplate = input.policy_template + ? pkgInfo.policy_templates?.find( + (policyTemplate) => policyTemplate.name === input.policy_template + ) + : pkgInfo.policy_templates?.[0]; + + if (!input.enabled || !packagePolicyTemplate || !packagePolicyTemplate.inputs?.length) { return undefined; } - const packageInputs = pkgInfo.policy_templates[0].inputs; + const packageInputs = packagePolicyTemplate.inputs; const packageInput = packageInputs.find((pkgInput) => pkgInput.type === input.type); if (!packageInput) { throw new Error(`Input template not found, unable to find input type ${input.type}`); diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 3735cfffeaa715..e69e38c1872846 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -45,6 +45,7 @@ const PackagePolicyBaseSchema = { inputs: schema.arrayOf( schema.object({ type: schema.string(), + policy_template: schema.maybe(schema.string()), enabled: schema.boolean(), keep_enabled: schema.maybe(schema.boolean()), vars: schema.maybe(ConfigRecordSchema), diff --git a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html index 4e0c13442d2674..b2363ffbaa6411 100644 --- a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html +++ b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html @@ -9,4 +9,5 @@ initial-filter="initialFilter" initialPageSize="initialPageSize" core-start="coreStart" + class="kbnAppWrapper" > diff --git a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index 6c2c79baafe0a5..7fa63404a4abd8 100644 --- a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -86,8 +86,8 @@ function GuidancePanelComponent(props: GuidancePanelProps) { const kibana = useKibana(); const { services, overlays } = kibana; - const { savedObjects, uiSettings, chrome, application } = services; - if (!overlays || !chrome || !application) return null; + const { savedObjects, uiSettings, application } = services; + if (!overlays || !application) return null; const onOpenDatasourcePicker = () => { openSourceModal({ overlays, savedObjects, uiSettings }, onIndexPatternSelected); @@ -149,8 +149,9 @@ function GuidancePanelComponent(props: GuidancePanelProps) { ); if (noIndexPatterns) { - const managementUrl = chrome.navLinks.get('kibana:stack_management')!.url; - const indexPatternUrl = `${managementUrl}/kibana/indexPatterns`; + const indexPatternUrl = application.getUrlForApp('management', { + path: '/kibana/indexPatterns', + }); const sampleDataUrl = `${application.getUrlForApp('home')}#/tutorial_directory/sampleData`; content = ( diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx index 77b0372e249944..b99d828b8bbd02 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; import { licensingMock } from '../../../../licensing/public/mocks'; import { App } from '../../../public/application/app'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts index 36f04be3b30b1d..18af96dd2804b1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts @@ -5,15 +5,17 @@ * 2.0. */ +import { act } from 'react-dom/test-utils'; + +import { getDefaultHotPhasePolicy, POLICY_NAME } from '../edit_policy/constants'; +import { setupEnvironment } from '../helpers'; + import { AppTestBed, getDoubleEncodedPolicyEditPath, getEncodedPolicyEditPath, setup, } from './app.helpers'; -import { setupEnvironment } from '../helpers/setup_environment'; -import { getDefaultHotPhasePolicy, POLICY_NAME } from '../edit_policy/constants'; -import { act } from 'react-dom/test-utils'; const SPECIAL_CHARS_NAME = 'test?#$+=&@:'; const PERCENT_SIGN_NAME = 'test%'; 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 58da73942000df..6f1c58b2e9b18b 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 @@ -8,8 +8,9 @@ import { TestBedConfig } from '@kbn/test/jest'; import { AppServicesContext } from '../../../public/types'; +import { Phase } from '../../../common/types'; + import { - Phase, createNodeAllocationActions, createFormToggleAction, createFormSetValueAction, @@ -27,7 +28,6 @@ import { createReadonlyActions, createIndexPriorityActions, } from '../helpers'; - import { initTestBed } from './init_test_bed'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts index 6403cfead8bd05..0c24101461f248 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts @@ -5,16 +5,16 @@ * 2.0. */ +import { act } from 'react-dom/test-utils'; +import { API_BASE_PATH } from '../../../../common/constants'; +import { setupEnvironment } from '../../helpers'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; import { DELETE_PHASE_POLICY, getDefaultHotPhasePolicy, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME, } from '../constants'; -import { act } from 'react-dom/test-utils'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { API_BASE_PATH } from '../../../../common/constants'; -import { setupEnvironment } from '../../helpers/setup_environment'; describe(' delete phase', () => { let testBed: EditPolicyTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts index 3736e9c43e2698..982377e2a03659 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' frozen phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts index 7ca68bd76759ea..136865329eb50e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { CloudNodeAllocationTestBed, setupCloudNodeAllocation, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts index 3b968c1dca4abe..e2093e04b77798 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { NodeAllocationTestBed, setupColdPhaseNodeAllocation } from './cold_phase.helpers'; describe(' node allocation in the cold phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts index d45e1cfe9567b8..cac6c174b769ee 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { GeneralNodeAllocationTestBed, setupGeneralNodeAllocation, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts index 1a1ed988d266e4..8d4d0afcf052d6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { NodeAllocationTestBed, setupWarmPhaseNodeAllocation } from './warm_phase.helpers'; describe(' node allocation in the warm phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts index de9a1b403ad331..1cb895e9ac86ab 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts @@ -6,8 +6,8 @@ */ import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { setupEnvironment } from '../../helpers/setup_environment'; describe(' request flyout', () => { let testBed: EditPolicyTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts index b58fe4086c7f01..8e9586e52577b9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' rollover', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts index 21a76b16c17efd..d400966cdae38f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy } from '../constants'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts index 5a117caa993786..a4f2a24bcee8b1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' timeline', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts index 8ec2f2e1b45986..ae9f3064838209 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' warm phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts index 669bcfdd34f638..9a14571c6ec3ba 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' cold phase validation', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts index 5c70a9025df670..0a047714bd3459 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' error indicators', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts index 879aab872b19bb..296b128eb8f527 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' hot phase validation', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts index 41d2b6d166dcee..08b794466da49b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; import { getGeneratedPolicies } from '../constants'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts index c164605e5cd4bb..ac11e8a162e02a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts @@ -8,8 +8,9 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { PhaseWithTiming } from '../../../../common/types'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { setupEnvironment } from '../../helpers/setup_environment'; describe(' timing validation', () => { let testBed: EditPolicyTestBed; @@ -69,12 +70,12 @@ describe(' timing validation', () => { ['warm', 'cold', 'delete', 'frozen'].forEach((phase: string) => { const { name, value, error } = testConfig; test(`${phase}: ${name}`, async () => { - await actions.togglePhase(phase as 'warm' | 'cold' | 'delete' | 'frozen'); + await actions.togglePhase(phase as PhaseWithTiming); // 1. We first set as dummy value to have a starting min_age value - await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue('111'); + await actions[phase as PhaseWithTiming].setMinAgeValue('111'); // 2. At this point we are sure there will be a change of value and that any validation // will be displayed under the field. - await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue(value); + await actions[phase as PhaseWithTiming].setMinAgeValue(value); actions.errors.waitForValidation(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts index 70794491d4edd8..bef99ea8cb891d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' warm phase validation', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index dfc81e9e0d9d79..8c345cf784f9f1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -6,14 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { licensingMock } from '../../../../../licensing/public/mocks'; +import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy, POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, } from '../constants'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { licensingMock } from '../../../../../licensing/public/mocks'; describe(' serialization', () => { let testBed: EditPolicyTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts index 8d5708950a75f6..a92747a95a2ca7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; const createWaitForValidationAction = (testBed: TestBed) => () => { const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts index 61b16d18648f5d..a7e4983165bacb 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts @@ -7,9 +7,9 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; const createFormCheckboxAction = (testBed: TestBed, dataTestSubject: string) => async ( checked: boolean diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts index 87b03ff4e1deb7..ad3d9d3bfbcb8d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts @@ -6,8 +6,8 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; -import { Phase } from '../types'; export const createFreezeActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts index a0423c12be5c3a..3b48da2a0c69fc 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts @@ -6,9 +6,9 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; export const createIndexPriorityActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts index 4a2491f3561ee1..551474c8b16e6c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts @@ -6,8 +6,8 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; export const createMinAgeActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts index b4b97b7c88a5ea..7c0f8fea7299d2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; import { DataTierAllocationType } from '../../../../public/application/sections/edit_policy/types'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createNodeAllocationActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts index ee9161c91209e5..b76143eccf1f25 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts @@ -6,8 +6,8 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; -import { Phase } from '../types'; export const createReadonlyActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts index d7ccd326947599..f1a64c3943511b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; export const createSearchableSnapshotActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts index cabd3012001b74..b07d7783379fbc 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts @@ -7,7 +7,7 @@ import { TestBed } from '@kbn/test/jest'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts index 9d227ef2624a49..05318503841243 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts @@ -6,9 +6,9 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; export const createShrinkActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts index fb1575d05bdef0..a0bed0e1644e68 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts @@ -6,9 +6,9 @@ */ import { TestBed } from '@kbn/test/jest'; - import { act } from 'react-dom/test-utils'; -import { Phase } from '../types'; + +import { Phase } from '../../../../common/types'; const toggleDeletePhase = async (testBed: TestBed) => { const { find, component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 5b2e1827cd6e83..f9cd1f27b72be9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { Phase } from './types'; - export * from './actions'; + +export { setupEnvironment } from './setup_environment'; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 385751f31d5bc0..b08f87e5a172bd 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -7,8 +7,14 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; +export type Phase = keyof Phases; + export type PhaseWithAllocation = 'warm' | 'cold'; +export type PhaseWithTiming = keyof Omit; + +export type PhaseExceptDelete = keyof Omit; + export interface SerializedPolicy { name: string; phases: Phases; @@ -22,8 +28,6 @@ export interface Phases { delete?: SerializedDeletePhase; } -export type PhasesExceptDelete = keyof Omit; - export interface PolicyFromES { modified_date: string; name: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx index 22422ceab8a040..0428c2939050d7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiText, EuiButtonGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { PhasesExceptDelete } from '../../../../../../common/types'; +import { PhaseExceptDelete } from '../../../../../../common/types'; import { usePhaseTimings } from '../../form'; @@ -32,7 +32,7 @@ const keepDataLabel = i18n.translate( ); interface Props { - phase: PhasesExceptDelete; + phase: PhaseExceptDelete; } export const PhaseFooter: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx index 040baf3625eb8a..f5ab67c0b3ee7b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx @@ -20,7 +20,7 @@ import { import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PhasesExceptDelete } from '../../../../../../../common/types'; +import { PhaseExceptDelete } from '../../../../../../../common/types'; import { ToggleField, useFormData } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; import { FormInternal } from '../../../types'; @@ -33,7 +33,7 @@ import { PhaseErrorIndicator } from './phase_error_indicator'; import './phase.scss'; interface Props { - phase: PhasesExceptDelete; + phase: PhaseExceptDelete; /** * Settings that should always be visible on the phase when it is enabled. */ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx index 47d2aa6ba92df8..0f5288df361196 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx @@ -10,15 +10,16 @@ import React, { FunctionComponent, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiTextColor } from '@elastic/eui'; -import { NumericField } from '../../../../../../shared_imports'; +import { PhaseExceptDelete } from '../../../../../../../common/types'; +import { NumericField } from '../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../edit_policy_context'; -import { UseField } from '../../../form'; +import { UseField } from '../../../form'; import { LearnMoreLink, DescribedFormRow } from '../..'; interface Props { - phase: 'hot' | 'warm' | 'cold' | 'frozen'; + phase: PhaseExceptDelete; } export const IndexPriorityField: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx index 136a37140cca7c..ad92ef2d44479a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -21,13 +21,12 @@ import { EuiIconTip, } from '@elastic/eui'; +import { PhaseWithTiming } from '../../../../../../../../common/types'; import { getFieldValidityAndErrorMessage, useFormData } from '../../../../../../../shared_imports'; import { UseField, useConfiguration, useGlobalFields } from '../../../../form'; import { getPhaseMinAgeInMilliseconds } from '../../../../lib'; import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; -type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; - const i18nTexts = { daysOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel', { defaultMessage: 'days', @@ -77,7 +76,7 @@ const i18nTexts = { }; interface Props { - phase: PhaseWithMinAgeAction; + phase: PhaseWithTiming; } export const MinAgeField: FunctionComponent = ({ phase }): React.ReactElement => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts index a626a0a62f5515..5f1b98c6dbda21 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts @@ -6,12 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import { Phases } from '../../../../../../../../common/types'; +import { PhaseWithTiming } from '../../../../../../../../common/types'; -type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; - -export function getUnitsAriaLabelForPhase(phase: keyof Phases) { - // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. +export function getUnitsAriaLabelForPhase(phase: PhaseWithTiming) { switch (phase) { case 'warm': return i18n.translate( @@ -29,6 +26,14 @@ export function getUnitsAriaLabelForPhase(phase: keyof Phases) { } ); + case 'frozen': + return i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeUnitsAriaLabel', + { + defaultMessage: 'Units for timing of frozen phase', + } + ); + case 'delete': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel', @@ -38,8 +43,7 @@ export function getUnitsAriaLabelForPhase(phase: keyof Phases) { ); } } -export function getTimingLabelForPhase(phase: PhaseWithMinAgeAction) { - // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. +export function getTimingLabelForPhase(phase: PhaseWithTiming) { switch (phase) { case 'warm': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', { @@ -51,6 +55,11 @@ export function getTimingLabelForPhase(phase: PhaseWithMinAgeAction) { defaultMessage: 'Timing for cold phase', }); + case 'frozen': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeLabel', { + defaultMessage: 'Timing for frozen phase', + }); + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 6edc6568ef766d..de5c258ab82fa7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiIconTip } from '@elastic/eui'; -import { PhasesExceptDelete } from '../../../../../../common/types'; +import { PhaseExceptDelete } from '../../../../../../common/types'; import { calculateRelativeFromAbsoluteMilliseconds, @@ -136,7 +136,7 @@ export const Timeline: FunctionComponent = memo( const widths = calculateWidths(phaseAgeInMilliseconds); - const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => + const getDurationInPhaseContent = (phase: PhaseExceptDelete): string | React.ReactNode => phaseAgeInMilliseconds.phases[phase] === Infinity ? ( ) : null; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index f24d350432d80e..c26f54cbb6f5a5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; +import { PhaseExceptDelete, PhaseWithTiming } from '../../../../../common/types'; import { FormSchema, fieldValidators } from '../../../../shared_imports'; import { defaultIndexPriority } from '../../../constants'; import { ROLLOVER_FORM_PATHS, CLOUD_DEFAULT_REPO } from '../constants'; -import { MinAgePhase } from '../types'; import { i18nTexts } from '../i18n_texts'; import { ifExistsNumberGreaterThanZero, @@ -107,8 +107,8 @@ const numberOfShardsField = { serializer: serializers.stringToNumber, }; -const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ - defaultValue: defaultIndexPriority[phase] as any, +const getPriorityField = (phase: PhaseExceptDelete) => ({ + defaultValue: defaultIndexPriority[phase], label: i18nTexts.editPolicy.indexPriorityFieldLabel, validations: [ { @@ -119,7 +119,7 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ serializer: serializers.stringToNumber, }); -const getMinAgeField = (phase: MinAgePhase, defaultValue?: string) => ({ +const getMinAgeField = (phase: PhaseWithTiming, defaultValue?: string) => ({ defaultValue, // By passing an empty array we make sure to *not* trigger the validation when the field value changes. // The validation will be triggered when the millisecond variant (in the _meta) is updated (in sync) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts index b39b3f3cd4226b..892e40f80f4b82 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts @@ -16,8 +16,8 @@ import { import { ROLLOVER_FORM_PATHS } from '../constants'; import { i18nTexts } from '../i18n_texts'; -import { PolicyFromES } from '../../../../../common/types'; -import { FormInternal, MinAgePhase } from '../types'; +import { PhaseWithTiming, PolicyFromES } from '../../../../../common/types'; +import { FormInternal } from '../types'; const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators; @@ -164,7 +164,7 @@ export const createPolicyNameValidations = ({ * For example, the user can't define '5 days' for cold phase if the * warm phase is set to '10 days'. */ -export const minAgeGreaterThanPreviousPhase = (phase: MinAgePhase) => ({ +export const minAgeGreaterThanPreviousPhase = (phase: PhaseWithTiming) => ({ formData, }: { formData: Record; @@ -173,7 +173,7 @@ export const minAgeGreaterThanPreviousPhase = (phase: MinAgePhase) => ({ return; } - const getValueFor = (_phase: MinAgePhase) => { + const getValueFor = (_phase: PhaseWithTiming) => { const milli = formData[`_meta.${_phase}.minAgeToMilliSeconds`]; const esFormat = diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 9d55f542db4c47..71e2741c27cdce 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -22,17 +22,16 @@ import moment from 'moment'; -import { splitSizeAndUnits } from '../../../lib/policies'; +import { Phase, PhaseWithTiming } from '../../../../../common/types'; -import { FormInternal, MinAgePhase } from '../types'; +import { splitSizeAndUnits } from '../../../lib/policies'; +import { FormInternal } from '../types'; /* -===- Private functions and types -===- */ -type Phase = 'hot' | MinAgePhase; - const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'frozen', 'delete']; -const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ +const getMinAge = (phase: PhaseWithTiming, formData: FormInternal) => ({ min_age: formData.phases?.[phase]?.min_age ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit : '0ms', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 987cbbcfde45b4..ba7d31cf6da49e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -18,8 +18,6 @@ export interface MinAgeField { minAgeToMilliSeconds: number; } -export type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete'; - export interface ForcemergeFields { bestCompression: boolean; } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx index 88948e9a7615b8..49003af28f3f1b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiButtonGroup } from '@elastic/eui'; +import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui'; import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types'; import { DatatableVisualizationState } from '../visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks'; @@ -212,4 +212,64 @@ describe('data table dimension editor', () => { expect(instance.find(PalettePanelContainer).exists()).toBe(true); }); + + it('should show the summary field for non numeric columns', () => { + const instance = mountWithIntl(); + expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_function"]').exists()).toBe( + false + ); + expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false); + }); + + it('should set the summary row function default to "none"', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_function"]') + .find(EuiComboBox) + .prop('selectedOptions') + ).toEqual([{ value: 'none', label: 'None' }]); + + expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false); + }); + + it('should show the summary row label input ony when summary row is different from "none"', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns[0].summaryRow = 'sum'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_function"]') + .find(EuiComboBox) + .prop('selectedOptions') + ).toEqual([{ value: 'sum', label: 'Sum' }]); + + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_label"]') + .find(EuiFieldText) + .prop('value') + ).toBe('Sum'); + }); + + it("should show the correct summary row name when user's changes summary label", () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns[0].summaryRow = 'sum'; + state.columns[0].summaryLabel = 'MySum'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_function"]') + .find(EuiComboBox) + .prop('selectedOptions') + ).toEqual([{ value: 'sum', label: 'Sum' }]); + + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_label"]') + .find(EuiFieldText) + .prop('value') + ).toBe('MySum'); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx index 76c47a9c743c51..6c39a04ae1504c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, @@ -16,10 +16,12 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonEmpty, + EuiFieldText, + EuiComboBox, } from '@elastic/eui'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { VisualizationDimensionEditorProps } from '../../types'; -import { DatatableVisualizationState } from '../visualization'; +import { ColumnState, DatatableVisualizationState } from '../visualization'; import { getOriginalId } from '../transpose_helpers'; import { CustomizablePalette, @@ -27,14 +29,22 @@ import { defaultPaletteParams, FIXED_PROGRESSION, getStopsForFixedMode, + useDebouncedValue, } from '../../shared_components/'; import { PalettePanelContainer } from './palette_panel_container'; import { findMinMaxByColumnId } from './shared_utils'; import './dimension_editor.scss'; +import { + getDefaultSummaryLabel, + getFinalSummaryConfiguration, + getSummaryRowOptions, +} from '../summary'; +import { isNumericField } from '../utils'; const idPrefix = htmlIdGenerator()(); type ColumnType = DatatableVisualizationState['columns'][number]; +type SummaryRowType = Extract; function updateColumnWith( state: DatatableVisualizationState, @@ -58,6 +68,24 @@ export function TableDimensionEditor( const { state, setState, frame, accessor } = props; const column = state.columns.find(({ columnId }) => accessor === columnId); const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const onSummaryLabelChangeToDebounce = useCallback( + (newSummaryLabel: string | undefined) => { + setState({ + ...state, + columns: updateColumnWith(state, accessor, { summaryLabel: newSummaryLabel }), + }); + }, + [accessor, setState, state] + ); + const { inputValue: summaryLabel, handleInputChange: onSummaryLabelChange } = useDebouncedValue< + string | undefined + >( + { + onChange: onSummaryLabelChangeToDebounce, + value: column?.summaryLabel, + }, + { allowEmptyString: true } // empty string is a valid label for this feature + ); if (!column) return null; if (column.isTransposed) return null; @@ -65,13 +93,16 @@ export function TableDimensionEditor( const currentData = frame.activeData?.[state.layerId]; // either read config state or use same logic as chart itself - const isNumericField = - currentData?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) - ?.meta.type === 'number'; - - const currentAlignment = column?.alignment || (isNumericField ? 'right' : 'left'); + const isNumeric = isNumericField(currentData, accessor); + const currentAlignment = column?.alignment || (isNumeric ? 'right' : 'left'); const currentColorMode = column?.colorMode || 'none'; const hasDynamicColoring = currentColorMode !== 'none'; + // when switching from one operation to another, make sure to keep the configuration consistent + const { summaryRow, summaryLabel: fallbackSummaryLabel } = getFinalSummaryConfiguration( + accessor, + column, + currentData + ); const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length; @@ -175,7 +206,61 @@ export function TableDimensionEditor( /> )} - {isNumericField && ( + {isNumeric && ( + <> + + { + const newValue = choices[0].value as SummaryRowType; + setState({ + ...state, + columns: updateColumnWith(state, accessor, { summaryRow: newValue }), + }); + }} + /> + + {summaryRow !== 'none' && ( + + { + onSummaryLabelChange(e.target.value); + }} + /> + + )} + + )} + {isNumeric && ( <> { c: { min: 3, max: 3 }, }); }); + + test('it does render a summary footer if at least one column has it configured', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-a"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').first().text()).toEqual( + 'Sum: 3' + ); + }); + + test('it does render a summary footer with just the raw value for empty label', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').first().text()).toEqual('3'); + }); + + test('it does not render the summary row if the only column with summary is hidden', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index e6fcf3f321f7f3..cd990149fdaf55 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -43,6 +43,7 @@ import { } from './table_actions'; import { findMinMaxByColumnId } from './shared_utils'; import { CUSTOM_PALETTE } from '../../shared_components/coloring/constants'; +import { getFinalSummaryConfiguration } from '../summary'; export const DataContext = React.createContext({}); @@ -286,6 +287,40 @@ export const DatatableComponent = (props: DatatableRenderProps) => { [onEditAction, sortBy, sortDirection] ); + const renderSummaryRow = useMemo(() => { + const columnsWithSummary = columnConfig.columns + .filter((col) => !!col.columnId && !col.hidden) + .map((config) => ({ + columnId: config.columnId, + summaryRowValue: config.summaryRowValue, + ...getFinalSummaryConfiguration(config.columnId, config, firstTable), + })) + .filter(({ summaryRow }) => summaryRow !== 'none'); + + if (columnsWithSummary.length) { + const summaryLookup = Object.fromEntries( + columnsWithSummary.map(({ summaryRowValue, summaryLabel, columnId }) => [ + columnId, + summaryLabel === '' ? `${summaryRowValue}` : `${summaryLabel}: ${summaryRowValue}`, + ]) + ); + return ({ columnId }: { columnId: string }) => { + const currentAlignment = alignments && alignments[columnId]; + const alignmentClassName = `lnsTableCell--${currentAlignment}`; + const columnName = + columns.find(({ id }) => id === columnId)?.displayAsText?.replace(/ /g, '-') || columnId; + return summaryLookup[columnId] != null ? ( +
+ {summaryLookup[columnId]} +
+ ) : null; + }; + } + }, [columnConfig.columns, alignments, firstTable, columns]); + if (isEmpty) { return ; } @@ -323,6 +358,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { sorting={sorting} onColumnResize={onColumnResize} toolbarVisibility={false} + renderFooterCellValue={renderSummaryRow} /> diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 2d5f4aea988562..79a541b0288ab6 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -28,10 +28,12 @@ import { ColumnState } from './visualization'; import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } from '../types'; import type { DatatableRender } from './components/types'; import { transposeTable } from './transpose_helpers'; +import { computeSummaryRowForColumn } from './summary'; export type ColumnConfigArg = Omit & { type: 'lens_datatable_column'; palette?: PaletteOutput; + summaryRowValue?: unknown; }; export interface Args { @@ -116,6 +118,16 @@ export const getDatatable = ({ return memo; }, {}); + const columnsWithSummary = args.columns.filter((c) => c.summaryRow); + for (const column of columnsWithSummary) { + column.summaryRowValue = computeSummaryRowForColumn( + column, + firstTable, + formatters, + formatFactory({ id: 'number' }) + ); + } + if (sortBy && columnsReverseLookup[sortBy] && sortDirection !== 'none') { // Sort on raw values for these types, while use the formatted value for the rest const sortingCriteria = getSortingCriteria( @@ -173,6 +185,8 @@ export const datatableColumn: ExpressionFunctionDefinition< types: ['palette'], help: '', }, + summaryRow: { types: ['string'], help: '' }, + summaryLabel: { types: ['string'], help: '' }, }, fn: function fn(input: unknown, args: ColumnState) { return { diff --git a/x-pack/plugins/lens/public/datatable_visualization/summary.test.ts b/x-pack/plugins/lens/public/datatable_visualization/summary.test.ts new file mode 100644 index 00000000000000..f92c83fbbfdc89 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/summary.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IFieldFormat } from 'src/plugins/data/public'; +import { Datatable } from 'src/plugins/expressions'; +import { computeSummaryRowForColumn, getFinalSummaryConfiguration } from './summary'; + +describe('Summary row helpers', () => { + const mockNumericTable: Datatable = { + type: 'datatable', + columns: [{ id: 'myColumn', name: 'My Column', meta: { type: 'number' } }], + rows: [{ myColumn: 45 }], + }; + + const mockNumericTableWithArray: Datatable = { + type: 'datatable', + columns: [{ id: 'myColumn', name: 'My Column', meta: { type: 'number' } }], + rows: [{ myColumn: [45, 90] }], + }; + + const mockNonNumericTable: Datatable = { + type: 'datatable', + columns: [{ id: 'myColumn', name: 'My Column', meta: { type: 'string' } }], + rows: [{ myColumn: 'myString' }], + }; + + const defaultFormatter = { convert: (x) => x } as IFieldFormat; + const customNumericFormatter = { convert: (x: number) => x.toFixed(2) } as IFieldFormat; + + describe('getFinalSummaryConfiguration', () => { + it('should return the base configuration for an unconfigured column', () => { + expect(getFinalSummaryConfiguration('myColumn', {}, mockNumericTable)).toEqual({ + summaryRow: 'none', + summaryLabel: 'None', + }); + }); + + it('should return the right configuration for a partially configured column', () => { + expect( + getFinalSummaryConfiguration('myColumn', { summaryRow: 'sum' }, mockNumericTable) + ).toEqual({ + summaryRow: 'sum', + summaryLabel: 'Sum', + }); + }); + + it('should return the base configuration for a transitioned invalid column', () => { + expect( + getFinalSummaryConfiguration('myColumn', { summaryRow: 'sum' }, mockNumericTableWithArray) + ).toEqual({ + summaryRow: 'sum', + summaryLabel: 'Sum', + }); + }); + + it('should return the base configuration for a non numeric column', () => { + expect( + getFinalSummaryConfiguration('myColumn', { summaryRow: 'sum' }, mockNonNumericTable) + ).toEqual({ + summaryRow: 'none', + summaryLabel: 'None', + }); + }); + }); + + describe('computeSummaryRowForColumn', () => { + for (const op of ['avg', 'sum', 'min', 'max'] as const) { + it(`should return formatted value for a ${op} summary function`, () => { + expect( + computeSummaryRowForColumn( + { summaryRow: op, columnId: 'myColumn', type: 'lens_datatable_column' }, + mockNumericTable, + { + myColumn: customNumericFormatter, + }, + defaultFormatter + ) + ).toBe('45.00'); + }); + } + + it('should ignore the column formatter, rather return the raw value for count operation', () => { + expect( + computeSummaryRowForColumn( + { summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' }, + mockNumericTable, + { + myColumn: customNumericFormatter, + }, + defaultFormatter + ) + ).toBe(1); + }); + + it('should only count non-null/empty values', () => { + expect( + computeSummaryRowForColumn( + { summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' }, + { ...mockNumericTable, rows: [...mockNumericTable.rows, { myColumn: null }] }, + { + myColumn: customNumericFormatter, + }, + defaultFormatter + ) + ).toBe(1); + }); + + it('should count numeric arrays as valid and distinct values', () => { + expect( + computeSummaryRowForColumn( + { summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' }, + mockNumericTableWithArray, + { + myColumn: defaultFormatter, + }, + defaultFormatter + ) + ).toBe(2); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/summary.ts b/x-pack/plugins/lens/public/datatable_visualization/summary.ts new file mode 100644 index 00000000000000..6c267445aab761 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/summary.ts @@ -0,0 +1,127 @@ +/* + * 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 { FieldFormat } from 'src/plugins/data/public'; +import { Datatable } from 'src/plugins/expressions/public'; +import { ColumnConfigArg } from './datatable_visualization'; +import { getOriginalId } from './transpose_helpers'; +import { isNumericField } from './utils'; + +type SummaryRowType = Extract; + +export function getFinalSummaryConfiguration( + columnId: string, + columnArgs: Pick | undefined, + table: Datatable | undefined +) { + const isNumeric = isNumericField(table, columnId); + + const summaryRow = isNumeric ? columnArgs?.summaryRow || 'none' : 'none'; + const summaryLabel = columnArgs?.summaryLabel ?? getDefaultSummaryLabel(summaryRow); + + return { + summaryRow, + summaryLabel, + }; +} + +export function getDefaultSummaryLabel(type: SummaryRowType) { + return getSummaryRowOptions().find(({ value }) => type === value)!.label!; +} + +export function getSummaryRowOptions(): Array<{ + value: SummaryRowType; + label: string; + 'data-test-subj': string; +}> { + return [ + { + value: 'none', + label: i18n.translate('xpack.lens.table.summaryRow.none', { + defaultMessage: 'None', + }), + 'data-test-subj': 'lns-datatable-summary-none', + }, + { + value: 'count', + label: i18n.translate('xpack.lens.table.summaryRow.count', { + defaultMessage: 'Value count', + }), + 'data-test-subj': 'lns-datatable-summary-count', + }, + { + value: 'sum', + label: i18n.translate('xpack.lens.table.summaryRow.sum', { + defaultMessage: 'Sum', + }), + 'data-test-subj': 'lns-datatable-summary-sum', + }, + { + value: 'avg', + label: i18n.translate('xpack.lens.table.summaryRow.average', { + defaultMessage: 'Average', + }), + 'data-test-subj': 'lns-datatable-summary-avg', + }, + { + value: 'min', + label: i18n.translate('xpack.lens.table.summaryRow.minimum', { + defaultMessage: 'Minimum', + }), + 'data-test-subj': 'lns-datatable-summary-min', + }, + { + value: 'max', + label: i18n.translate('xpack.lens.table.summaryRow.maximum', { + defaultMessage: 'Maximum', + }), + 'data-test-subj': 'lns-datatable-summary-max', + }, + ]; +} + +export function computeSummaryRowForColumn( + columnArgs: ColumnConfigArg, + table: Datatable, + formatters: Record, + defaultFormatter: FieldFormat +) { + const summaryValue = computeFinalValue(columnArgs.summaryRow, columnArgs.columnId, table.rows); + // ignore the coluymn formatter for the count case + if (columnArgs.summaryRow === 'count') { + return defaultFormatter.convert(summaryValue); + } + return formatters[getOriginalId(columnArgs.columnId)].convert(summaryValue); +} + +function computeFinalValue( + type: ColumnConfigArg['summaryRow'], + columnId: string, + rows: Datatable['rows'] +) { + // flatten the row structure, to easier handle numeric arrays + const validRows = rows.filter((v) => v[columnId] != null).flatMap((v) => v[columnId]); + const count = validRows.length; + const sum = validRows.reduce((partialSum: number, value: number) => { + return partialSum + value; + }, 0); + switch (type) { + case 'sum': + return sum; + case 'count': + return count; + case 'avg': + return sum / count; + case 'min': + return Math.min(...validRows); + case 'max': + return Math.max(...validRows); + default: + throw Error('No summary function found'); + } +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/utils.ts b/x-pack/plugins/lens/public/datatable_visualization/utils.ts new file mode 100644 index 00000000000000..64fdee233e8304 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/utils.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable } from 'src/plugins/expressions/public'; +import { getOriginalId } from './transpose_helpers'; + +function isValidNumber(value: unknown): boolean { + return typeof value === 'number' || value == null; +} + +export function isNumericField(currentData: Datatable | undefined, accessor: string) { + const isNumeric = + currentData?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) + ?.meta.type === 'number'; + + return ( + isNumeric && + currentData?.rows.every((row) => { + const val = row[accessor]; + return isValidNumber(val) || (Array.isArray(val) && val.every(isValidNumber)); + }) + ); +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index efde4160019e73..e48cb1b28c084c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -23,6 +23,7 @@ import { TableDimensionEditor } from './components/dimension_editor'; import { CUSTOM_PALETTE } from '../shared_components/coloring/constants'; import { CustomPaletteParams } from '../shared_components/coloring/types'; import { getStopsForFixedMode } from '../shared_components'; +import { getDefaultSummaryLabel } from './summary'; export interface ColumnState { columnId: string; @@ -38,6 +39,8 @@ export interface ColumnState { alignment?: 'left' | 'right' | 'center'; palette?: PaletteOutput; colorMode?: 'none' | 'cell' | 'text'; + summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max'; + summaryLabel?: string; } export interface SortingState { @@ -358,6 +361,8 @@ export const getDatatableVisualization = ({ reverse: false, // managed at UI level }; + const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none'; + return { type: 'expression', chain: [ @@ -376,6 +381,10 @@ export const getDatatableVisualization = ({ alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], colorMode: [column.colorMode ?? 'none'], palette: [paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams)], + summaryRow: hasNoSummaryRow ? [] : [column.summaryRow!], + summaryLabel: hasNoSummaryRow + ? [] + : [column.summaryLabel ?? getDefaultSummaryLabel(column.summaryRow!)], }, }, ], diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.test.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.test.ts new file mode 100644 index 00000000000000..7aa93fcad95e95 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { useDebouncedValue } from './debounced_value'; + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +describe('useDebouncedValue', () => { + it('should update upstream value changes', () => { + const onChangeMock = jest.fn(); + const { result } = renderHook(() => useDebouncedValue({ value: 'a', onChange: onChangeMock })); + + act(() => { + result.current.handleInputChange('b'); + }); + + expect(onChangeMock).toHaveBeenCalledWith('b'); + }); + + it('should fallback to initial value with empty string (by default)', () => { + const onChangeMock = jest.fn(); + const { result } = renderHook(() => useDebouncedValue({ value: 'a', onChange: onChangeMock })); + + act(() => { + result.current.handleInputChange(''); + }); + + expect(onChangeMock).toHaveBeenCalledWith('a'); + }); + + it('should allow empty input to be updated', () => { + const onChangeMock = jest.fn(); + const { result } = renderHook(() => + useDebouncedValue({ value: 'a', onChange: onChangeMock }, { allowEmptyString: true }) + ); + + act(() => { + result.current.handleInputChange(''); + }); + + expect(onChangeMock).toHaveBeenCalledWith(''); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts index 1f8ba0fa765b2e..5525f6b16b3161 100644 --- a/x-pack/plugins/lens/public/shared_components/debounced_value.ts +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts @@ -13,15 +13,19 @@ import { debounce } from 'lodash'; * are in flight because the user is currently modifying the value. */ -export const useDebouncedValue = ({ - onChange, - value, -}: { - onChange: (val: T) => void; - value: T; -}) => { +export const useDebouncedValue = ( + { + onChange, + value, + }: { + onChange: (val: T) => void; + value: T; + }, + { allowEmptyString }: { allowEmptyString?: boolean } = {} +) => { const [inputValue, setInputValue] = useState(value); const unflushedChanges = useRef(false); + const shouldUpdateWithEmptyString = Boolean(allowEmptyString); // Save the initial value const initialValue = useRef(value); @@ -45,7 +49,10 @@ export const useDebouncedValue = ({ const handleInputChange = (val: T) => { setInputValue(val); - onChangeDebounced(val || initialValue.current); + const valueToUpload = shouldUpdateWithEmptyString + ? val ?? initialValue.current + : val || initialValue.current; + onChangeDebounced(valueToUpload); }; return { inputValue, handleInputChange, initialValue: initialValue.current }; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 92bac61f3fab3e..f34172765e1ddf 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,6 @@ "embeddable", "uiActions", "kibanaLegacy", - "indexPatternManagement", "discover", "triggersActionsUi" ], diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 8375b0a0b1dfc5..d20042ddd94433 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -8,20 +8,21 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; import { isEqual } from 'lodash'; import { - EuiPanel, - EuiPopover, - EuiContextMenuPanel, + EuiButtonEmpty, EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, EuiFlexGroup, EuiFlexItem, + EuiPanel, + EuiPopover, EuiSelect, - EuiTitle, EuiSpacer, - EuiContextMenuItem, - EuiButtonEmpty, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import useDebounce from 'react-use/lib/useDebounce'; import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; import { AddSwimlaneToDashboardControl } from './dashboard_controls/add_swimlane_to_dashboard_controls'; import { useMlKibana } from '../contexts/kibana'; @@ -93,6 +94,18 @@ export const AnomalyTimeline: FC = React.memo( viewBySwimlaneData, } = explorerState; + const [severityUpdate, setSeverityUpdate] = useState(swimLaneSeverity); + + useDebounce( + () => { + if (severityUpdate === swimLaneSeverity) return; + + explorerService.setSwimLaneSeverity(severityUpdate!); + }, + 500, + [severityUpdate, swimLaneSeverity] + ); + const annotations = useMemo(() => overallAnnotations.annotationsData, [overallAnnotations]); const menuItems = useMemo(() => { @@ -194,9 +207,9 @@ export const AnomalyTimeline: FC = React.memo( { - explorerService.setSwimLaneSeverity(update); + setSeverityUpdate(update); }, [])} /> diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx index d2780691e551d9..408e86512ed6dc 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/edit_job.tsx @@ -51,9 +51,9 @@ export const EditJob: FC = ({ job, jobOverride, existingGroupIds, ); const handleValidation = () => { - const jobGroupsValidationResult = - formState.jobGroups ?? - [].map((group) => groupValidator(group)).filter((result) => result !== null); + const jobGroupsValidationResult = (formState.jobGroups ?? []) + .map((group) => groupValidator(group)) + .filter((result) => result !== null); setValidationResult({ jobGroups: jobGroupsValidationResult, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index b97c16e02d9007..0daf4f28f33d31 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -87,12 +87,11 @@ export const JobItem: FC = memo( - {jobGroups ?? - [].map((group) => ( - - {group} - - ))} + {(jobGroups ?? []).map((group) => ( + + {group} + + ))} {setupResult && setupResult.error && ( diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 42440883408ae5..1191f3b253fd7d 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -24,14 +24,12 @@ import type { } from 'src/plugins/share/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; -import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import type { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import type { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; import type { LicenseManagementUIPluginSetup } from '../../license_management/public'; import type { LicensingPluginSetup } from '../../licensing/public'; @@ -78,7 +76,6 @@ export interface MlSetupDependencies { uiActions: UiActionsSetup; kibanaVersion: string; share: SharePluginSetup; - indexPatternManagement: IndexPatternManagementSetup; triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup; alerting?: AlertingSetup; } @@ -148,12 +145,6 @@ export class MlPlugin implements Plugin { if (pluginsSetup.home) { registerFeature(pluginsSetup.home); } - - // register ML for the index pattern management no data screen. - pluginsSetup.indexPatternManagement.environment.update({ - ml: () => - capabilities.ml.canFindFileStructure ? MlCardState.ENABLED : MlCardState.HIDDEN, - }); } else { // if ml is disabled in elasticsearch, disable ML in kibana this.appUpdater$.next(() => ({ diff --git a/x-pack/plugins/ml/public/register_feature.ts b/x-pack/plugins/ml/public/register_feature.ts index 191e064c85b900..e8d64360df1230 100644 --- a/x-pack/plugins/ml/public/register_feature.ts +++ b/x-pack/plugins/ml/public/register_feature.ts @@ -13,10 +13,6 @@ import { import { PLUGIN_ID } from '../common/constants/app'; export const registerFeature = (home: HomePublicPluginSetup) => { - // register ML for the kibana home screen. - // so the file data visualizer appears to allow people to import data - home.environment.update({ ml: true }); - // register ML so it appears on the Kibana home page home.featureCatalogue.register({ id: PLUGIN_ID, @@ -37,19 +33,4 @@ export const registerFeature = (home: HomePublicPluginSetup) => { solutionId: 'kibana', order: 500, }); - - home.featureCatalogue.register({ - id: `${PLUGIN_ID}_file_data_visualizer`, - title: i18n.translate('xpack.ml.fileDataVisualizerTitle', { - defaultMessage: 'Upload a file', - }), - description: i18n.translate('xpack.ml.fileDataVisualizerDescription', { - defaultMessage: 'Import your own CSV, NDJSON, or log file.', - }), - icon: 'document', - path: '/app/ml/filedatavisualizer', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - order: 520, - }); }; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json index a4412a6d732e99..3ff28ef3d8df3d 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json @@ -13,10 +13,29 @@ "term": { "event.category": "network" } - }, + } + ], + "must": [ { - "term": { - "event.outcome": "deny" + "bool": { + "should": [ + { + "match": { + "event.outcome": { + "query": "deny", + "operator": "OR" + } + } + }, + { + "match": { + "event.type": { + "query": "denied", + "operator": "OR" + } + } + } + ] } } ] diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 2a95557473fc09..f20c6c2d52fdd8 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -53,6 +53,7 @@ export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; export const UI_SETTINGS_DATEFORMAT_TZ = 'dateFormat:tz'; export const LAYOUT_TYPES = { + CANVAS: 'canvas', PRESERVE_LAYOUT: 'preserve_layout', PRINT: 'print', }; diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap new file mode 100644 index 00000000000000..a6753211fba3b3 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -0,0 +1,1191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout option 1`] = ` + +
+ +
+

+ + + Analytical Apps can take a minute or two to generate based upon the size of your test-object-type. + + +

+
+
+ +
+ + + } + labelType="label" + > +
+
+ + } + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + > +
+ + + + + Full page layout + + + +
+
+ +
+ + + Remove borders and footer logo + + +
+
+
+
+
+ + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+

+ + + Alternatively, copy this POST URL to call generation from outside Kibana or from Watcher. + + +

+
+
+ +
+ + + + + + + + + + + + +
+
+ +
+
+ +
+ +`; + +exports[`ScreenCapturePanelContent properly renders a view with "print" layout option 1`] = ` + +
+ +
+

+ + + Analytical Apps can take a minute or two to generate based upon the size of your test-object-type. + + +

+
+
+ +
+ + + } + labelType="label" + > +
+
+ + } + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + > +
+ + + + + Optimize for printing + + + +
+
+ +
+ + + Uses multiple pages, showing at most 2 visualizations per page + + +
+
+
+
+
+ + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+

+ + + Alternatively, copy this POST URL to call generation from outside Kibana or from Watcher. + + +

+
+
+ +
+ + + + + + + + + + + + +
+
+ +
+
+ +
+ +`; + +exports[`ScreenCapturePanelContent renders the default view properly 1`] = ` + +
+ +
+

+ + + Analytical Apps can take a minute or two to generate based upon the size of your test-object-type. + + +

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

+ + + Alternatively, copy this POST URL to call generation from outside Kibana or from Watcher. + + +

+
+
+ +
+ + + + + + + + + + + + +
+
+ +
+
+ +
+ +`; diff --git a/x-pack/plugins/reporting/public/components/index.ts b/x-pack/plugins/reporting/public/components/index.ts index 4adcc347dc7043..b8cccda2a6613b 100644 --- a/x-pack/plugins/reporting/public/components/index.ts +++ b/x-pack/plugins/reporting/public/components/index.ts @@ -11,3 +11,4 @@ export { getWarningFormulasToast } from './job_warning_formulas'; export { getWarningMaxSizeToast } from './job_warning_max_size'; export { getGeneralErrorToast } from './general_error'; export { ScreenCapturePanelContent } from './screen_capture_panel_content'; +export { getSharedComponents } from './shared'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index a38c37c8086893..4d7828b7894072 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -5,7 +5,17 @@ * 2.0. */ -import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; +import { + EuiAccordion, + EuiButton, + EuiCopy, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; import { ToastsSetup } from 'src/core/public'; @@ -20,14 +30,12 @@ export interface Props { toasts: ToastsSetup; reportType: string; - /** - * Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. - */ + /** Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. **/ requiresSavedState: boolean; layoutId: string | undefined; objectId?: string; getJobParams: () => BaseParams; - options?: ReactElement; + options?: ReactElement | null; isDirty?: boolean; onClose?: () => void; intl: InjectedIntl; @@ -110,50 +118,61 @@ class ReportingPanelContentUi extends Component { ); } - const reportMsg = ( - - ); - return ( -

{reportMsg}

+

+ +

{this.props.options} {this.renderGenerateReportButton(false)} - - -

- -

-
- + - - {(copy) => ( - + + + +

- - )} - +

+
+ + + + {(copy) => ( + + + + )} + +
); } @@ -247,44 +266,12 @@ class ReportingPanelContentUi extends Component { } }) .catch((error: any) => { - if (error.message === 'not exportable') { - return this.props.toasts.addWarning({ - title: intl.formatMessage( - { - id: 'xpack.reporting.panelContent.whatCanBeExportedWarningTitle', - defaultMessage: 'Only saved {objectType} can be exported', - }, - { objectType: this.state.objectType } - ), - text: toMountPoint( - - ), - }); - } - - const defaultMessage = - error?.res?.status === 403 ? ( - - ) : ( - - ); - - this.props.toasts.addDanger({ + this.props.toasts.addError(error, { title: intl.formatMessage({ id: 'xpack.reporting.panelContent.notification.reportingErrorTitle', - defaultMessage: 'Reporting error', + defaultMessage: 'Failed to create report', }), - text: toMountPoint(error.message || defaultMessage), - 'data-test-subj': 'queueReportError', + toastMessage: error.body.message, }); }); }; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx new file mode 100644 index 00000000000000..84a6dcc3c0ba3f --- /dev/null +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { IntlProvider } from 'react-intl'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { BaseParams } from '../../common/types'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ScreenCapturePanelContent } from './screen_capture_panel_content'; + +const getJobParamsDefault: () => BaseParams = () => ({ + objectType: 'test-object-type', + title: 'Test Report Title', + browserTimezone: 'America/New_York', +}); + +test('ScreenCapturePanelContent renders the default view properly', () => { + const coreSetup = coreMock.createSetup(); + const component = mount( + + + + ); + expect(component.find('EuiForm')).toMatchSnapshot(); + expect(component.text()).not.toMatch('Full page layout'); + expect(component.text()).not.toMatch('Optimize for printing'); +}); + +test('ScreenCapturePanelContent properly renders a view with "canvas" layout option', () => { + const coreSetup = coreMock.createSetup(); + const component = mount( + + + + ); + expect(component.find('EuiForm')).toMatchSnapshot(); + expect(component.text()).toMatch('Full page layout'); +}); + +test('ScreenCapturePanelContent properly renders a view with "print" layout option', () => { + const coreSetup = coreMock.createSetup(); + const component = mount( + + + + ); + expect(component.find('EuiForm')).toMatchSnapshot(); + expect(component.text()).toMatch('Optimize for printing'); +}); diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index 21497ed2891562..fd6003f8656e87 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -5,11 +5,13 @@ * 2.0. */ -import { EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Component, Fragment } from 'react'; +import moment from 'moment'; +import React, { Component } from 'react'; import { ToastsSetup } from 'src/core/public'; -import { BaseParams } from '../../common/types'; +import { getDefaultLayoutSelectors } from '../../common'; +import { BaseParams, LayoutParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingPanelContent } from './reporting_panel_content'; @@ -17,33 +19,33 @@ export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; + layoutOption?: 'canvas' | 'print'; objectId?: string; getJobParams: () => BaseParams; + requiresSavedState: boolean; isDirty?: boolean; onClose?: () => void; } interface State { - isPreserveLayoutSupported: boolean; usePrintLayout: boolean; + useCanvasLayout: boolean; } export class ScreenCapturePanelContent extends Component { constructor(props: Props) { super(props); - const { objectType } = props.getJobParams(); - const isPreserveLayoutSupported = props.reportType !== 'png' && objectType !== 'visualization'; this.state = { - isPreserveLayoutSupported, usePrintLayout: false, + useCanvasLayout: false, }; } public render() { return ( { } private renderOptions = () => { - if (this.state.isPreserveLayoutSupported) { + if (this.props.layoutOption === 'print') { return ( - + + } + > { onChange={this.handlePrintLayoutChange} data-test-subj="usePrintLayout" /> - - + ); } - return ( - - - - ); + if (this.props.layoutOption === 'canvas') { + return ( + + } + > + + } + checked={this.state.useCanvasLayout} + onChange={this.handleCanvasLayoutChange} + data-test-subj="reportModeToggle" + /> + + ); + } + + return null; }; private handlePrintLayoutChange = (evt: EuiSwitchEvent) => { - this.setState({ usePrintLayout: evt.target.checked }); + this.setState({ usePrintLayout: evt.target.checked, useCanvasLayout: false }); }; - private getLayout = () => { + private handleCanvasLayoutChange = (evt: EuiSwitchEvent) => { + this.setState({ useCanvasLayout: evt.target.checked, usePrintLayout: false }); + }; + + private getLayout = (): Required => { + const { layout: outerLayout } = this.props.getJobParams(); + + let dimensions = outerLayout?.dimensions; + if (!dimensions) { + const el = document.querySelector('[data-shared-items-container]'); + const { height, width } = el ? el.getBoundingClientRect() : { height: 768, width: 1024 }; + dimensions = { height, width }; + } + + let selectors = outerLayout?.selectors; + if (!selectors) { + selectors = getDefaultLayoutSelectors(); + } + if (this.state.usePrintLayout) { - return { id: 'print' }; + return { id: 'print', dimensions, selectors }; } - const el = document.querySelector('[data-shared-items-container]'); - const bounds = el ? el.getBoundingClientRect() : { height: 768, width: 1024 }; + if (this.state.useCanvasLayout) { + return { id: 'canvas', dimensions, selectors }; + } - return { - id: this.props.reportType === 'png' ? 'png' : 'preserve_layout', - dimensions: { - height: bounds.height, - width: bounds.width, - }, - }; + return { id: 'preserve_layout', dimensions, selectors }; }; - private getJobParams = () => { + private getJobParams = (): Required => { + const outerParams = this.props.getJobParams(); + let browserTimezone = outerParams.browserTimezone; + if (!browserTimezone) { + browserTimezone = moment.tz.guess(); + } + return { ...this.props.getJobParams(), layout: this.getLayout(), + browserTimezone, }; }; } diff --git a/x-pack/plugins/reporting/public/components/shared/get_shared_components.tsx b/x-pack/plugins/reporting/public/components/shared/get_shared_components.tsx new file mode 100644 index 00000000000000..12d70c87019754 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/shared/get_shared_components.tsx @@ -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. + */ + +import { CoreSetup } from 'kibana/public'; +import React from 'react'; +import { ReportingAPIClient } from '../..'; +import { PDF_REPORT_TYPE } from '../../../common/constants'; +import type { Props as PanelPropsScreenCapture } from '../screen_capture_panel_content'; +import { ScreenCapturePanelContent } from '../screen_capture_panel_content_lazy'; + +interface IncludeOnCloseFn { + onClose: () => void; +} + +type PropsPDF = Pick & IncludeOnCloseFn; + +/* + * As of 7.14, the only shared component is a PDF report that is suited for Canvas integration. + * This is not planned to expand, as work is to be done on moving the export-type implementations out of Reporting + * Related Discuss issue: https://github.com/elastic/kibana/issues/101422 + */ +export function getSharedComponents(core: CoreSetup) { + return { + ReportingPanelPDF(props: PropsPDF) { + return ( + + ); + }, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/index.tsx b/x-pack/plugins/reporting/public/components/shared/index.tsx similarity index 80% rename from x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/index.tsx rename to x-pack/plugins/reporting/public/components/shared/index.tsx index d9b3bcd80752dc..592538f1b84ccf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/index.tsx +++ b/x-pack/plugins/reporting/public/components/shared/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export * from './use_platform'; +export { getSharedComponents } from './get_shared_components'; diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 7c82fda7554d4c..7179a81664b6f7 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -7,24 +7,20 @@ import { PluginInitializerContext } from 'src/core/public'; import { getDefaultLayoutSelectors } from '../common'; -import { ScreenCapturePanelContent } from './components/screen_capture_panel_content'; -import * as jobCompletionNotifications from './lib/job_completion_notifications'; +import { getSharedComponents } from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingPublicPlugin } from './plugin'; export interface ReportingSetup { - components: { - ScreenCapturePanel: typeof ScreenCapturePanelContent; - }; getDefaultLayoutSelectors: typeof getDefaultLayoutSelectors; - ReportingAPIClient: typeof ReportingAPIClient; usesUiCapabilities: () => boolean; + components: ReturnType; } export type ReportingStart = ReportingSetup; export { constants, getDefaultLayoutSelectors } from '../common'; -export { ReportingAPIClient, ReportingPublicPlugin as Plugin, jobCompletionNotifications }; +export { ReportingAPIClient, ReportingPublicPlugin as Plugin }; export function plugin(initializerContext: PluginInitializerContext) { return new ReportingPublicPlugin(initializerContext); diff --git a/x-pack/plugins/reporting/public/mocks.ts b/x-pack/plugins/reporting/public/mocks.ts new file mode 100644 index 00000000000000..414d1b0ae70fe3 --- /dev/null +++ b/x-pack/plugins/reporting/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { coreMock } from 'src/core/public/mocks'; +import { ReportingSetup } from '.'; +import { getDefaultLayoutSelectors } from '../common'; +import { getSharedComponents } from './components/shared'; + +type Setup = jest.Mocked; + +const createSetupContract = (): Setup => { + const coreSetup = coreMock.createSetup(); + return { + getDefaultLayoutSelectors: jest.fn().mockImplementation(getDefaultLayoutSelectors), + usesUiCapabilities: jest.fn().mockImplementation(() => true), + components: getSharedComponents(coreSetup), + }; +}; + +export const reportingPluginMock = { + createSetupContract, + createStartContract: createSetupContract, +}; diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index ff0d425faf54a5..577732fdb13927 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -29,10 +29,7 @@ import { constants, getDefaultLayoutSelectors } from '../common'; import { durationToNumber } from '../common/schema_utils'; import { JobId, JobSummarySet } from '../common/types'; import { ReportingSetup, ReportingStart } from './'; -import { - getGeneralErrorToast, - ScreenCapturePanelContent as ScreenCapturePanel, -} from './components'; +import { getGeneralErrorToast, getSharedComponents } from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action'; @@ -86,7 +83,6 @@ export class ReportingPublicPlugin ReportingPublicPluginSetupDendencies, ReportingPublicPluginStartDendencies > { - private readonly contract: ReportingStart; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -95,21 +91,30 @@ export class ReportingPublicPlugin defaultMessage: 'Reporting', }); private config: ClientConfigType; + private contract?: ReportingSetup; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + } + + private getContract(core?: CoreSetup) { + if (core) { + this.contract = { + getDefaultLayoutSelectors, + usesUiCapabilities: () => this.config.roles?.enabled === false, + components: getSharedComponents(core), + }; + } - this.contract = { - ReportingAPIClient, - components: { ScreenCapturePanel }, - getDefaultLayoutSelectors, - usesUiCapabilities: () => this.config.roles?.enabled === false, - }; + if (!this.contract) { + throw new Error(`Setup error in Reporting plugin!`); + } + + return this.contract; } public setup(core: CoreSetup, setupDeps: ReportingPublicPluginSetupDendencies) { - const { http, notifications, getStartServices, uiSettings } = core; - const { toasts } = notifications; + const { http, getStartServices, uiSettings } = core; const { home, management, @@ -163,6 +168,9 @@ export class ReportingPublicPlugin new ReportingCsvPanelAction({ core, startServices$, license$, usesUiCapabilities }) ); + const reportingStart = this.getContract(core); + const { toasts } = core.notifications; + share.register( ReportingCsvShareProvider({ apiClient, @@ -173,6 +181,7 @@ export class ReportingPublicPlugin usesUiCapabilities, }) ); + share.register( reportingScreenshotShareProvider({ apiClient, @@ -184,7 +193,7 @@ export class ReportingPublicPlugin }) ); - return this.contract; + return reportingStart; } public start(core: CoreStart) { @@ -203,7 +212,7 @@ export class ReportingPublicPlugin ) .subscribe(); - return this.contract; + return this.getContract(); } public stop() { diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index fc732b0ec7dfbe..7fe5268fc99102 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -65,13 +65,7 @@ export const ReportingCsvShareProvider = ({ ? moment.tz.guess() : uiSettings.get('dateFormat:tz'); - const getShareMenuItems = ({ - objectType, - objectId, - sharingData, - onClose, - isDirty, - }: ShareContext) => { + const getShareMenuItems = ({ objectType, objectId, sharingData, onClose }: ShareContext) => { if ('search' !== objectType) { return []; } @@ -114,7 +108,6 @@ export const ReportingCsvShareProvider = ({ layoutId={undefined} objectId={objectId} getJobParams={getJobParams} - isDirty={isDirty} onClose={onClose} /> ), diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index f4a952ef58298e..42f6ee5fcb898b 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -11,7 +11,7 @@ import React from 'react'; import * as Rx from 'rxjs'; import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { CoreStart } from 'src/core/public'; -import type { ShareContext } from '../../../../../src/plugins/share/public'; +import { ShareContext } from 'src/plugins/share/public'; import type { LicensingPluginSetup } from '../../../licensing/public'; import type { LayoutParams } from '../../common/types'; import type { JobParamsPNG } from '../../server/export_types/png/types'; @@ -167,6 +167,7 @@ export const reportingScreenshotShareProvider = ({ toasts={toasts} reportType="png" objectId={objectId} + requiresSavedState={true} getJobParams={getPngJobParams({ shareableUrl, apiClient, @@ -203,6 +204,8 @@ export const reportingScreenshotShareProvider = ({ toasts={toasts} reportType="printablePdf" objectId={objectId} + requiresSavedState={true} + layoutOption={objectType === 'dashboard' ? 'print' : undefined} getJobParams={getPdfJobParams({ shareableUrl, apiClient, diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index 8b99ac3c5761b5..034c25758a1f0c 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -6,3 +6,4 @@ */ export { SecurityLicense } from './licensing'; +export { AuthenticatedUser } from './model'; diff --git a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts index ba3297aeb5493e..ba73d3b196d2f8 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts @@ -8,10 +8,10 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { apiKeysMock } from './api_keys/api_keys.mock'; -import type { AuthenticationServiceStart } from './authentication_service'; +import type { AuthenticationServiceStartInternal } from './authentication_service'; export const authenticationServiceMock = { - createStart: (): DeeplyMockedKeys => ({ + createStart: (): DeeplyMockedKeys => ({ apiKeys: apiKeysMock.create(), login: jest.fn(), logout: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index e5895422e7a743..946fedbeee04ff 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -51,7 +51,7 @@ interface AuthenticationServiceStartParams { loggers: LoggerFactory; } -export interface AuthenticationServiceStart { +export interface AuthenticationServiceStartInternal extends AuthenticationServiceStart { apiKeys: Pick< APIKeys, | 'areAPIKeysEnabled' @@ -66,6 +66,21 @@ export interface AuthenticationServiceStart { getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; } +/** + * Authentication services available on the security plugin's start contract. + */ +export interface AuthenticationServiceStart { + apiKeys: Pick< + APIKeys, + | 'areAPIKeysEnabled' + | 'create' + | 'invalidate' + | 'grantAsInternalUser' + | 'invalidateAsInternalUser' + >; + getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; +} + export class AuthenticationService { private license!: SecurityLicense; private authenticator?: Authenticator; @@ -212,7 +227,7 @@ export class AuthenticationService { legacyAuditLogger, loggers, session, - }: AuthenticationServiceStartParams): AuthenticationServiceStart { + }: AuthenticationServiceStartParams): AuthenticationServiceStartInternal { const apiKeys = new APIKeys({ clusterClient, logger: this.logger.get('api-key'), diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index f7f5f82d2858b1..4f82c5653baa9a 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -6,7 +6,11 @@ */ export { canRedirectRequest } from './can_redirect_request'; -export { AuthenticationService, AuthenticationServiceStart } from './authentication_service'; +export { + AuthenticationService, + AuthenticationServiceStart, + AuthenticationServiceStartInternal, +} from './authentication_service'; export { AuthenticationResult } from './authentication_result'; export { DeauthenticationResult } from './deauthentication_result'; export { diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx index 1e2588dafe2337..c5adb3aff670e0 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.tsx +++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx @@ -72,7 +72,7 @@ interface AuthorizationServiceStartParams { online$: Observable; } -export interface AuthorizationServiceSetup { +export interface AuthorizationServiceSetupInternal extends AuthorizationServiceSetup { actions: Actions; checkPrivilegesWithRequest: CheckPrivilegesWithRequest; checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; @@ -82,6 +82,21 @@ export interface AuthorizationServiceSetup { privileges: PrivilegesService; } +/** + * Authorization services available on the setup contract of the security plugin. + */ +export interface AuthorizationServiceSetup { + /** + * Actions are used to create the "actions" that are associated with Elasticsearch's + * application privileges, and are used to perform the authorization checks implemented + * by the various `checkPrivilegesWithRequest` derivatives + */ + actions: Actions; + checkPrivilegesWithRequest: CheckPrivilegesWithRequest; + checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; + mode: AuthorizationMode; +} + export class AuthorizationService { private logger!: Logger; private applicationName!: string; @@ -101,7 +116,7 @@ export class AuthorizationService { kibanaIndexName, getSpacesService, getCurrentUser, - }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { + }: AuthorizationServiceSetupParams): AuthorizationServiceSetupInternal { this.logger = loggers.get('authorization'); this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 16a3c2ae50058c..4d67f3435e7dab 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -6,6 +6,10 @@ */ export { Actions } from './actions'; -export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service'; +export { + AuthorizationService, + AuthorizationServiceSetup, + AuthorizationServiceSetupInternal, +} from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { CheckPrivilegesPayload } from './types'; diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index e50ab66a92547e..a48c6833096ccb 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -25,6 +25,7 @@ export type { InvalidateAPIKeysParams, InvalidateAPIKeyResult, GrantAPIKeyResult, + AuthenticationServiceStart, } from './authentication'; export type { CheckPrivilegesPayload } from './authorization'; export type AuthorizationServiceSetup = SecurityPluginStart['authz']; @@ -32,6 +33,7 @@ export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; export { ROUTE_TAG_CAN_REDIRECT } from './routes/tags'; +export { AuditServiceSetup } from './audit'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 57be308525fddd..98f1335b534505 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -10,7 +10,6 @@ import { combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import type { TypeOf } from '@kbn/config-schema'; -import type { RecursiveReadonly } from '@kbn/utility-types'; import type { CoreSetup, CoreStart, @@ -36,10 +35,14 @@ import type { AnonymousAccessServiceStart } from './anonymous_access'; import { AnonymousAccessService } from './anonymous_access'; import type { AuditServiceSetup } from './audit'; import { AuditService, SecurityAuditLogger } from './audit'; -import type { AuthenticationServiceStart } from './authentication'; +import type { + AuthenticationServiceStart, + AuthenticationServiceStartInternal, +} from './authentication'; import { AuthenticationService } from './authentication'; import type { AuthorizationServiceSetup } from './authorization'; import { AuthorizationService } from './authorization'; +import type { AuthorizationServiceSetupInternal } from './authorization/authorization_service'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; import { ElasticsearchService } from './elasticsearch'; @@ -74,11 +77,14 @@ export interface SecurityPluginSetup { /** * @deprecated Use `authz` methods from the `SecurityServiceStart` contract instead. */ - authz: Pick< - AuthorizationServiceSetup, - 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' - >; + authz: AuthorizationServiceSetup; + /** + * Exposes information about the available security features under the current license. + */ license: SecurityLicense; + /** + * Exposes services for audit logging. + */ audit: AuditServiceSetup; } @@ -86,11 +92,14 @@ export interface SecurityPluginSetup { * Describes public Security plugin contract returned at the `start` stage. */ export interface SecurityPluginStart { - authc: Pick; - authz: Pick< - AuthorizationServiceSetup, - 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' - >; + /** + * Authentication services to confirm the user is who they say they are. + */ + authc: AuthenticationServiceStart; + /** + * Authorization services to manage and access the permissions a particular user has. + */ + authz: AuthorizationServiceSetup; } export interface PluginSetupDependencies { @@ -113,14 +122,9 @@ export interface PluginStartDependencies { * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ export class SecurityPlugin - implements - Plugin< - RecursiveReadonly, - RecursiveReadonly, - PluginSetupDependencies - > { + implements Plugin { private readonly logger: Logger; - private authorizationSetup?: AuthorizationServiceSetup; + private authorizationSetup?: AuthorizationServiceSetupInternal; private auditSetup?: AuditServiceSetup; private anonymousAccessStart?: AnonymousAccessServiceStart; private configSubscription?: Subscription; @@ -152,7 +156,7 @@ export class SecurityPlugin private readonly authenticationService = new AuthenticationService( this.initializerContext.logger.get('authentication') ); - private authenticationStart?: AuthenticationServiceStart; + private authenticationStart?: AuthenticationServiceStartInternal; private readonly getAuthentication = () => { if (!this.authenticationStart) { throw new Error(`authenticationStart is not registered!`); diff --git a/x-pack/plugins/security/server/routes/api_keys/create.test.ts b/x-pack/plugins/security/server/routes/api_keys/create.test.ts index 502a7cb1246c49..ee28681adbd5f2 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.test.ts @@ -12,7 +12,7 @@ import type { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRequestHandlerContext } from '../../types'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -28,7 +28,7 @@ describe('Create API Key route', () => { } let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { authc = authenticationServiceMock.createStart(); const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index da097faf8c6b27..1000e79563b571 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -12,7 +12,7 @@ import type { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRequestHandlerContext } from '../../types'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -28,7 +28,7 @@ describe('API keys enabled', () => { } let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { authc = authenticationServiceMock.createStart(); const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 1e0b6784a45c82..5acc5817bfb3e9 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -13,7 +13,7 @@ import { httpServerMock } from 'src/core/server/mocks'; import type { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { AuthenticationResult, DeauthenticationResult, @@ -28,7 +28,7 @@ import { defineCommonRoutes } from './common'; describe('Common authentication routes', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; let license: jest.Mocked; let mockContext: SecurityRequestHandlerContext; beforeEach(() => { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index 3b2497ed9f30ba..c28c435217ef3a 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -11,7 +11,7 @@ import type { RequestHandler, RouteConfig } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { AuthenticationResult, SAMLLogin } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRouter } from '../../types'; @@ -21,7 +21,7 @@ import { defineSAMLRoutes } from './saml'; describe('SAML authentication routes', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index cdb3a2fa289fdd..e36ca1b9ab72e5 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -10,8 +10,8 @@ import type { HttpResources, IBasePath, Logger } from 'src/core/server'; import type { KibanaFeature } from '../../../features/server'; import type { SecurityLicense } from '../../common/licensing'; -import type { AuthenticationServiceStart } from '../authentication'; -import type { AuthorizationServiceSetup } from '../authorization'; +import type { AuthenticationServiceStartInternal } from '../authentication'; +import type { AuthorizationServiceSetupInternal } from '../authorization'; import type { ConfigType } from '../config'; import type { SecurityFeatureUsageServiceStart } from '../feature_usage'; import type { Session } from '../session_management'; @@ -34,12 +34,12 @@ export interface RouteDefinitionParams { httpResources: HttpResources; logger: Logger; config: ConfigType; - authz: AuthorizationServiceSetup; + authz: AuthorizationServiceSetupInternal; getSession: () => PublicMethodsOf; license: SecurityLicense; getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; - getAuthenticationService: () => AuthenticationServiceStart; + getAuthenticationService: () => AuthenticationServiceStartInternal; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index a97fce7ea4b190..e5f4381a204212 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -15,8 +15,8 @@ import { kibanaResponseFactory } from 'src/core/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStart } from '../../authentication'; import { AuthenticationResult } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication/authentication_service'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { Session } from '../../session_management'; import { sessionMock } from '../../session_management/session.mock'; @@ -26,7 +26,7 @@ import { defineChangeUserPasswordRoutes } from './change_password'; describe('Change password', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; let session: jest.Mocked>; let routeHandler: RequestHandler; let routeConfig: RouteConfig; diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index e052fd1c7ab6a4..837b3c594d3968 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -9,7 +9,7 @@ import type { CoreSetup, LegacyRequest } from 'src/core/server'; import { KibanaRequest, SavedObjectsClient } from '../../../../../src/core/server'; import type { AuditServiceSetup, SecurityAuditLogger } from '../audit'; -import type { AuthorizationServiceSetup } from '../authorization'; +import type { AuthorizationServiceSetupInternal } from '../authorization/authorization_service'; import type { SpacesService } from '../plugin'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; @@ -17,7 +17,7 @@ interface SetupSavedObjectsParams { legacyAuditLogger: SecurityAuditLogger; audit: AuditServiceSetup; authz: Pick< - AuthorizationServiceSetup, + AuthorizationServiceSetupInternal, 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest' >; savedObjects: CoreSetup['savedObjects']; diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts index 0b8a7abab23828..bc7cb727edd805 100644 --- a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts @@ -15,7 +15,10 @@ import { spacesClientMock } from '../../../spaces/server/mocks'; import type { AuditEvent, AuditLogger } from '../audit'; import { SpaceAuditAction } from '../audit'; import { auditServiceMock } from '../audit/index.mock'; -import type { AuthorizationServiceSetup } from '../authorization'; +import type { + AuthorizationServiceSetup, + AuthorizationServiceSetupInternal, +} from '../authorization'; import { authorizationMock } from '../authorization/index.mock'; import type { CheckPrivilegesResponse } from '../authorization/types'; import type { LegacySpacesAuditLogger } from './legacy_audit_logger'; @@ -85,7 +88,9 @@ const setup = ({ securityEnabled = false }: Opts = {}) => { }; }; -const expectNoAuthorizationCheck = (authorization: jest.Mocked) => { +const expectNoAuthorizationCheck = ( + authorization: jest.Mocked +) => { expect(authorization.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); expect(authorization.checkPrivilegesWithRequest).not.toHaveBeenCalled(); expect(authorization.checkSavedObjectsPrivilegesWithRequest).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 91b48afdc4ed1e..87e99a4b472e7c 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -186,6 +186,7 @@ export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`; export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`; +export const DETECTION_ENGINE_RULES_BULK_ACTION = `${DETECTION_ENGINE_RULES_URL}/_bulk_action`; export const TIMELINE_URL = '/api/timeline'; export const TIMELINES_URL = '/api/timelines'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 7b49b68ab79a1d..c9a9d3bdcb24c1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -82,6 +82,8 @@ export const ruleIdOrUndefined = t.union([rule_id, t.undefined]); export type RuleIdOrUndefined = t.TypeOf; export const id = UUID; +export type Id = t.TypeOf; + export const idOrUndefined = t.union([id, t.undefined]); export type IdOrUndefined = t.TypeOf; @@ -408,3 +410,13 @@ export const privilege = t.type({ }); export type Privilege = t.TypeOf; + +export enum BulkAction { + 'enable' = 'enable', + 'disable' = 'disable', + 'export' = 'export', + 'delete' = 'delete', + 'duplicate' = 'duplicate', +} + +export const bulkAction = t.keyof(BulkAction); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts index 1035e9128305c2..7722feb5f080d7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts @@ -17,3 +17,4 @@ export * from './query_signals_index_schema'; export * from './set_signal_status_schema'; export * from './update_rules_bulk_schema'; export * from './rule_schemas'; +export * from './perform_bulk_action_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts new file mode 100644 index 00000000000000..cb78168fbec6e2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BulkAction } from '../common/schemas'; +import { PerformBulkActionSchema } from './perform_bulk_action_schema'; + +export const getPerformBulkActionSchemaMock = (): PerformBulkActionSchema => ({ + query: '', + action: BulkAction.disable, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts new file mode 100644 index 00000000000000..a9707b88f52406 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { performBulkActionSchema, PerformBulkActionSchema } from './perform_bulk_action_schema'; +import { exactCheck, foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { left } from 'fp-ts/lib/Either'; +import { BulkAction } from '../common/schemas'; + +describe('perform_bulk_action_schema', () => { + test('query and action is valid', () => { + const payload: PerformBulkActionSchema = { + query: 'name: test', + action: BulkAction.enable, + }; + + const decoded = performBulkActionSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('missing query is valid', () => { + const payload: PerformBulkActionSchema = { + query: undefined, + action: BulkAction.enable, + }; + + const decoded = performBulkActionSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('missing action is invalid', () => { + const payload: Omit = { + query: 'name: test', + }; + + const decoded = performBulkActionSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "action"', + ]); + expect(message.schema).toEqual({}); + }); + + test('unknown action is invalid', () => { + const payload: Omit & { action: 'unknown' } = { + query: 'name: test', + action: 'unknown', + }; + + const decoded = performBulkActionSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = foldLeftRight(checked); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "unknown" supplied to "action"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts new file mode 100644 index 00000000000000..adb26f107c8cd1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { bulkAction, queryOrUndefined } from '../common/schemas'; + +export const performBulkActionSchema = t.exact( + t.type({ + query: queryOrUndefined, + action: bulkAction, + }) +); + +export type PerformBulkActionSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 3c9be9a823c498..33072e8df5cec5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -47,6 +47,7 @@ export interface HostIsolationResponse { export interface PendingActionsResponse { agent_id: string; pending_actions: { + /** Number of actions pending for each type. The `key` could be one of the `ISOLATION_ACTIONS` values. */ [key: string]: number; }; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index e006944eb914fe..1e0d798cf7f07d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1114,3 +1114,15 @@ export interface GetExceptionSummaryResponse { macos: number; linux: number; } + +/** + * Supported React-Router state for the Generic List page + */ +export interface ListPageRouteState { + /** Where the user should be redirected to when the `Back` button is clicked */ + onBackButtonNavigateTo: Parameters; + /** The URL for the `Back` button */ + backButtonUrl?: string; + /** The label for the button */ + backButtonLabel?: string; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 8d66370fea4d36..94a2e7f236bebd 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -6,7 +6,6 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { ApplicationStart } from 'kibana/public'; import { DeleteTrustedAppsRequestSchema, @@ -133,15 +132,3 @@ export type TrustedApp = NewTrustedApp & { updated_at: string; updated_by: string; }; - -/** - * Supported React-Router state for the Trusted Apps List page - */ -export interface TrustedAppsListPageRouteState { - /** Where the user should be redirected to when the `Back` button is clicked */ - onBackButtonNavigateTo: Parameters; - /** The URL for the `Back` button */ - backButtonUrl?: string; - /** The label for the button */ - backButtonLabel?: string; -} diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 2f98cb15287d63..8210c7c6d8b20b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -80,7 +80,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, deleteFirstRule, deleteSelectedRules, editFirstRule, @@ -159,7 +159,7 @@ describe('Custom detection rules creation', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 0dbecde3d4d3f6..b38796cca373d3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -56,7 +56,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -113,7 +113,7 @@ describe('Detection rules, EQL', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); @@ -208,7 +208,7 @@ describe('Detection rules, sequence EQL', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index d0f2cd9f45743f..bc8cf0137fa830 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -78,7 +78,7 @@ import { scrollJsonViewToBottom, } from '../../tasks/alerts_details'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, duplicateFirstRule, duplicateSelectedRules, duplicateRuleFromMenu, @@ -424,7 +424,7 @@ describe('indicator match', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts index 0fe1326947a120..65dde40bbd76b8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts @@ -46,7 +46,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -90,7 +90,7 @@ describe('Detection rules, machine learning', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index eb10f32bb89897..f9f1ca14c81642 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -68,7 +68,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -121,7 +121,7 @@ describe('Detection rules, override', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index fb0a01bd1c7d30..74e1d082ae4103 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -8,23 +8,29 @@ import { COLLAPSED_ACTION_BTN, ELASTIC_RULES_BTN, + pageSelector, RELOAD_PREBUILT_RULES_BTN, - RULES_ROW, - RULES_TABLE, + RULES_EMPTY_PROMPT, + RULE_SWITCH, SHOWING_RULES_TEXT, } from '../../screens/alerts_detection_rules'; import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, deleteFirstRule, deleteSelectedRules, loadPrebuiltDetectionRules, - goToNextPage, reloadDeletedRules, selectNumberOfRules, waitForRulesTableToBeLoaded, waitForPrebuiltDetectionRulesToBeLoaded, + selectAllRules, + confirmRulesDelete, + activateSelectedRules, + waitForRuleToChangeStatus, + deactivateSelectedRules, + changeRowsPerPageTo, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -39,7 +45,9 @@ describe('Alerts rules, prebuilt rules', () => { }); it('Loads prebuilt rules', () => { + const rowsPerPage = 100; const expectedNumberOfRules = totalNumberOfPrebuiltRules; + const expectedNumberOfPages = Math.ceil(totalNumberOfPrebuiltRules / rowsPerPage); const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); @@ -51,23 +59,14 @@ describe('Alerts rules, prebuilt rules', () => { cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); - changeRowsPerPageTo300(); + changeRowsPerPageTo(rowsPerPage); cy.get(SHOWING_RULES_TEXT).should('have.text', `Showing ${expectedNumberOfRules} rules`); - cy.get(RULES_TABLE).then(($table1) => { - const firstScreenRules = $table1.find(RULES_ROW).length; - goToNextPage(); - cy.get(RULES_TABLE).then(($table2) => { - const secondScreenRules = $table2.find(RULES_ROW).length; - const totalNumberOfRules = firstScreenRules + secondScreenRules; - - expect(totalNumberOfRules).to.eql(expectedNumberOfRules); - }); - }); + cy.get(pageSelector(expectedNumberOfPages)).should('exist'); }); }); -describe('Deleting prebuilt rules', () => { +describe('Actions with prebuilt rules', () => { beforeEach(() => { const expectedNumberOfRules = totalNumberOfPrebuiltRules; const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; @@ -81,11 +80,30 @@ describe('Deleting prebuilt rules', () => { waitForPrebuiltDetectionRulesToBeLoaded(); cy.get(ELASTIC_RULES_BTN).should('have.text', expectedElasticRulesBtnText); + }); + + it('Allows to activate/deactivate all rules at once', () => { + selectAllRules(); + activateSelectedRules(); + waitForRuleToChangeStatus(); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - changeRowsPerPageTo300(); + selectAllRules(); + deactivateSelectedRules(); + waitForRuleToChangeStatus(); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'false'); + }); + + it('Allows to delete all rules at once', () => { + selectAllRules(); + deleteSelectedRules(); + confirmRulesDelete(); + cy.get(RULES_EMPTY_PROMPT).should('be.visible'); }); it('Does not allow to delete one rule when more than one is selected', () => { + changeRowsPerPageTo100(); + const numberOfRulesToBeSelected = 2; selectNumberOfRules(numberOfRulesToBeSelected); @@ -95,12 +113,14 @@ describe('Deleting prebuilt rules', () => { }); it('Deletes and recovers one rule', () => { + changeRowsPerPageTo100(); + const expectedNumberOfRulesAfterDeletion = totalNumberOfPrebuiltRules - 1; const expectedNumberOfRulesAfterRecovering = totalNumberOfPrebuiltRules; deleteFirstRule(); cy.reload(); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -114,7 +134,7 @@ describe('Deleting prebuilt rules', () => { cy.get(RELOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.reload(); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', @@ -123,6 +143,8 @@ describe('Deleting prebuilt rules', () => { }); it('Deletes and recovers more than one rule', () => { + changeRowsPerPageTo100(); + const numberOfRulesToBeSelected = 2; const expectedNumberOfRulesAfterDeletion = totalNumberOfPrebuiltRules - 2; const expectedNumberOfRulesAfterRecovering = totalNumberOfPrebuiltRules; @@ -130,7 +152,7 @@ describe('Deleting prebuilt rules', () => { selectNumberOfRules(numberOfRulesToBeSelected); deleteSelectedRules(); cy.reload(); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(RELOAD_PREBUILT_RULES_BTN).should('exist'); cy.get(RELOAD_PREBUILT_RULES_BTN).should( @@ -147,7 +169,7 @@ describe('Deleting prebuilt rules', () => { cy.get(RELOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.reload(); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); cy.get(ELASTIC_RULES_BTN).should( 'have.text', diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index 0cf3caa09814c2..f1ee0d39f545f5 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -31,7 +31,7 @@ import { resetAllRulesIdleModalTimeout, sortByActivatedRules, waitForRulesTableToBeLoaded, - waitForRuleToBeActivated, + waitForRuleToChangeStatus, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../../common/constants'; @@ -62,13 +62,13 @@ describe('Alerts detection rules', () => { .invoke('text') .then((secondInitialRuleName) => { activateRule(SECOND_RULE); - waitForRuleToBeActivated(); + waitForRuleToChangeStatus(); cy.get(RULE_NAME) .eq(FOURTH_RULE) .invoke('text') .then((fourthInitialRuleName) => { activateRule(FOURTH_RULE); - waitForRuleToBeActivated(); + waitForRuleToChangeStatus(); sortByActivatedRules(); cy.get(RULE_NAME) .eq(FIRST_RULE) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 7c09b311807beb..0f4095372f92a7 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -59,7 +59,7 @@ import { waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; import { - changeRowsPerPageTo300, + changeRowsPerPageTo100, filterByCustomRules, goToCreateNewRule, goToRuleDetails, @@ -113,7 +113,7 @@ describe('Detection rules, threshold', () => { cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeRowsPerPageTo300(); + changeRowsPerPageTo100(); const expectedNumberOfRules = 1; cy.get(RULES_TABLE).then(($table) => { diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 70dde344c88b6c..ba071184d98eba 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -25,6 +25,12 @@ export const DUPLICATE_RULE_MENU_PANEL_BTN = '[data-test-subj="rules-details-dup export const REFRESH_BTN = '[data-test-subj="refreshRulesAction"] button'; +export const ACTIVATE_RULE_BULK_BTN = '[data-test-subj="activateRuleBulk"]'; + +export const DEACTIVATE_RULE_BULK_BTN = '[data-test-subj="deactivateRuleBulk"]'; + +export const EXPORT_RULE_BULK_BTN = '[data-test-subj="exportRuleBulk"]'; + export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]'; export const DUPLICATE_RULE_BULK_BTN = '[data-test-subj="duplicateRuleBulk"]'; @@ -87,3 +93,11 @@ export const pageSelector = (pageNumber: number) => `[data-test-subj="pagination-button-${pageNumber - 1}"]`; export const NEXT_BTN = '[data-test-subj="pagination-button-next"]'; + +export const SELECT_ALL_RULES_BTN = '[data-test-subj="selectAllRules"]'; + +export const RULES_EMPTY_PROMPT = '[data-test-subj="rulesEmptyPrompt"]'; + +export const RULES_DELETE_CONFIRMATION_MODAL = '[data-test-subj="allRulesDeleteConfirmationModal"]'; + +export const MODAL_CONFIRMATION_BTN = '[data-test-subj="confirmModalConfirmButton"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index cc14c54a4d84e3..78298c98810772 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -36,6 +36,12 @@ import { DUPLICATE_RULE_MENU_PANEL_BTN, DUPLICATE_RULE_BULK_BTN, RULES_ROW, + SELECT_ALL_RULES_BTN, + MODAL_CONFIRMATION_BTN, + RULES_DELETE_CONFIRMATION_MODAL, + ACTIVATE_RULE_BULK_BTN, + DEACTIVATE_RULE_BULK_BTN, + EXPORT_RULE_BULK_BTN, } from '../screens/alerts_detection_rules'; import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details'; @@ -57,11 +63,6 @@ export const duplicateFirstRule = () => { cy.get(DUPLICATE_RULE_ACTION_BTN).click(); }; -export const duplicateSelectedRules = () => { - cy.get(BULK_ACTIONS_BTN).click({ force: true }); - cy.get(DUPLICATE_RULE_BULK_BTN).click(); -}; - /** * Duplicates the rule from the menu and does additional * pipes and checking that the elements are present on the @@ -106,6 +107,26 @@ export const deleteSelectedRules = () => { cy.get(DELETE_RULE_BULK_BTN).click(); }; +export const duplicateSelectedRules = () => { + cy.get(BULK_ACTIONS_BTN).click({ force: true }); + cy.get(DUPLICATE_RULE_BULK_BTN).click(); +}; + +export const activateSelectedRules = () => { + cy.get(BULK_ACTIONS_BTN).click({ force: true }); + cy.get(ACTIVATE_RULE_BULK_BTN).click(); +}; + +export const deactivateSelectedRules = () => { + cy.get(BULK_ACTIONS_BTN).click({ force: true }); + cy.get(DEACTIVATE_RULE_BULK_BTN).click(); +}; + +export const exportSelectedRules = () => { + cy.get(BULK_ACTIONS_BTN).click({ force: true }); + cy.get(EXPORT_RULE_BULK_BTN).click(); +}; + export const exportFirstRule = () => { cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); cy.get(EXPORT_ACTION_BTN).click(); @@ -149,6 +170,17 @@ export const selectNumberOfRules = (numberOfRules: number) => { } }; +export const selectAllRules = () => { + cy.get(SELECT_ALL_RULES_BTN).contains('Select all').click(); + cy.get(SELECT_ALL_RULES_BTN).contains('Clear'); +}; + +export const confirmRulesDelete = () => { + cy.get(RULES_DELETE_CONFIRMATION_MODAL).should('be.visible'); + cy.get(MODAL_CONFIRMATION_BTN).click(); + cy.get(RULES_DELETE_CONFIRMATION_MODAL).should('not.exist'); +}; + export const sortByActivatedRules = () => { cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); waitForRulesTableToBeRefreshed(); @@ -174,9 +206,10 @@ export const waitForRulesTableToBeAutoRefreshed = () => { export const waitForPrebuiltDetectionRulesToBeLoaded = () => { cy.get(LOAD_PREBUILT_RULES_BTN).should('not.exist'); cy.get(RULES_TABLE).should('exist'); + cy.get(RULES_TABLE_REFRESH_INDICATOR).should('not.exist'); }; -export const waitForRuleToBeActivated = () => { +export const waitForRuleToChangeStatus = () => { cy.get(RULE_SWITCH_LOADER).should('exist'); cy.get(RULE_SWITCH_LOADER).should('not.exist'); }; @@ -215,8 +248,8 @@ export const changeRowsPerPageTo = (rowsCount: number) => { waitForRulesTableToBeRefreshed(); }; -export const changeRowsPerPageTo300 = () => { - changeRowsPerPageTo(300); +export const changeRowsPerPageTo100 = () => { + changeRowsPerPageTo(100); }; export const goToPage = (pageNumber: number) => { diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx new file mode 100644 index 00000000000000..5cde22de697386 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiBadge, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface EndpointHostIsolationStatusProps { + isIsolated: boolean; + /** the count of pending isolate actions */ + pendingIsolate?: number; + /** the count of pending unisoalte actions */ + pendingUnIsolate?: number; +} + +/** + * Component will display a host isoaltion status based on whether it is currently isolated or there are + * isolate/unisolate actions pending. If none of these are applicable, no UI component will be rendered + * (`null` is returned) + */ +export const EndpointHostIsolationStatus = memo( + ({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0 }) => { + return useMemo(() => { + // If nothing is pending and host is not currently isolated, then render nothing + if (!isIsolated && !pendingIsolate && !pendingUnIsolate) { + return null; + } + + // If nothing is pending, but host is isolated, then show isolation badge + if (!pendingIsolate && !pendingUnIsolate) { + return ( + + + + ); + } + + // If there are multiple types of pending isolation actions, then show count of actions with tooltip that displays breakdown + // TODO:PT implement edge case + // if () { + // + // } + + // Show 'pending [un]isolate' depending on what's pending + return ( + + + {pendingIsolate ? ( + + ) : ( + + )} + + + ); + }, [isIsolated, pendingIsolate, pendingUnIsolate]); + } +); + +EndpointHostIsolationStatus.displayName = 'EndpointHostIsolationStatus'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts index f5387a1b1a99c6..bd8e23e3a4559f 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts @@ -8,3 +8,4 @@ export * from './isolate_success'; export * from './isolate_form'; export * from './unisolate_form'; +export * from './endpoint_host_isolation_status'; diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/generic_downloader/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 219be8cbda311f..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GenericDownloader renders correctly against snapshot 1`] = ``; diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx deleted file mode 100644 index b8066c836de722..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { shallow, mount } from 'enzyme'; -import React from 'react'; -import { GenericDownloaderComponent, ExportSelectedData } from './index'; -import { errorToToaster } from '../toasters'; - -jest.mock('../toasters', () => ({ - useStateToaster: jest.fn(() => [jest.fn(), jest.fn()]), - errorToToaster: jest.fn(), -})); - -describe('GenericDownloader', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('show toaster with correct error message if error occurrs', () => { - mount( - - ); - expect((errorToToaster as jest.Mock).mock.calls[0][0].title).toEqual('Failed to export data…'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx deleted file mode 100644 index 2a2e425702755e..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.tsx +++ /dev/null @@ -1,112 +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 React, { useEffect, useRef } from 'react'; -import styled from 'styled-components'; -import { isFunction } from 'lodash/fp'; -import * as i18n from './translations'; - -import { ExportDocumentsProps } from '../../../detections/containers/detection_engine/rules'; -import { useStateToaster, errorToToaster } from '../toasters'; -import { TimelineErrorResponse } from '../../../../common/types/timeline'; - -const InvisibleAnchor = styled.a` - display: none; -`; - -export type ExportSelectedData = ({ - excludeExportDetails, - filename, - ids, - signal, -}: ExportDocumentsProps) => Promise; - -export interface GenericDownloaderProps { - filename: string; - ids?: string[]; - exportSelectedData: ExportSelectedData; - onExportSuccess?: (exportCount: number) => void; - onExportFailure?: () => void; -} - -/** - * Component for downloading Rules as an exported .ndjson file. Download will occur on each update to `rules` param - * - * @param filename of file to be downloaded - * @param payload Rule[] - * - */ - -export const GenericDownloaderComponent = ({ - exportSelectedData, - filename, - ids, - onExportSuccess, - onExportFailure, -}: GenericDownloaderProps) => { - const anchorRef = useRef(null); - const [, dispatchToaster] = useStateToaster(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const exportData = async () => { - if (anchorRef && anchorRef.current && ids != null && ids.length > 0) { - try { - const exportResponse = await exportSelectedData({ - ids, - signal: abortCtrl.signal, - }); - - if (isSubscribed) { - // this is for supporting IE - if (isFunction(window.navigator.msSaveOrOpenBlob)) { - window.navigator.msSaveBlob(exportResponse); - } else { - const objectURL = window.URL.createObjectURL(exportResponse); - // These are safe-assignments as writes to anchorRef are isolated to exportData - anchorRef.current.href = objectURL; // eslint-disable-line require-atomic-updates - anchorRef.current.download = filename; // eslint-disable-line require-atomic-updates - anchorRef.current.click(); - - if (typeof window.URL.revokeObjectURL === 'function') { - window.URL.revokeObjectURL(objectURL); - } - } - if (onExportSuccess != null) { - onExportSuccess(ids.length); - } - } - } catch (error) { - if (isSubscribed) { - if (onExportFailure != null) { - onExportFailure(); - } - errorToToaster({ title: i18n.EXPORT_FAILURE, error, dispatchToaster }); - } - } - } - }; - - exportData(); - - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ids]); - - return ; -}; - -GenericDownloaderComponent.displayName = 'GenericDownloaderComponent'; - -export const GenericDownloader = React.memo(GenericDownloaderComponent); - -GenericDownloader.displayName = 'GenericDownloader'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_bool_state.ts b/x-pack/plugins/security_solution/public/common/hooks/use_bool_state.ts new file mode 100644 index 00000000000000..f9204de38d6808 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_bool_state.ts @@ -0,0 +1,33 @@ +/* + * 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 { useCallback, useState } from 'react'; + +type UseBoolStateReturn = [ + state: boolean, + setTrue: () => void, + setFalse: () => void, + toggle: () => void +]; + +export const useBoolState = (initial = false): UseBoolStateReturn => { + const [state, setState] = useState(initial); + + const setTrue = useCallback(() => { + setState(true); + }, []); + + const setFalse = useCallback(() => { + setState(false); + }, []); + + const toggle = useCallback(() => { + setState((val) => !val); + }, []); + + return [state, setTrue, setFalse, toggle]; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_value_changed.ts b/x-pack/plugins/security_solution/public/common/hooks/use_value_changed.ts new file mode 100644 index 00000000000000..ef054d05397579 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_value_changed.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useRef } from 'react'; + +/** + * Use this method to watch value for changes. + * + * CAUTION: you probably don't need this hook. Try to use useEffect first. + * It is only useful in rare cases when a value differs by reference but not by content between renders. + * + * @param callback A callback to call when the value changes + * @param nextValue A value to observe for changes + */ +export const useValueChanged = (callback: (value: T) => void, nextValue: T) => { + const prevValue = useRef(nextValue); + + useEffect(() => { + if (JSON.stringify(prevValue.current) !== JSON.stringify(nextValue)) { + prevValue.current = nextValue; + callback(nextValue); + } + }, [callback, nextValue]); +}; diff --git a/x-pack/plugins/security_solution/public/common/utils/download_blob.ts b/x-pack/plugins/security_solution/public/common/utils/download_blob.ts new file mode 100644 index 00000000000000..80f32a8bdaa0c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/download_blob.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Method for downloading any file + * + * @param blob raw data + * @param filename of file to be downloaded + * + */ +export const downloadBlob = (blob: Blob, filename: string) => { + const objectURL = window.URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = objectURL; + anchor.download = filename; + anchor.click(); + window.URL.revokeObjectURL(objectURL); + anchor.remove(); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx index 3400a960bbc60a..d1dfd6ccfd5657 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx @@ -21,7 +21,7 @@ describe('AllRulesTables', () => { { { ; - hasNoPermissions: boolean; + hasPermissions: boolean; monitoringColumns: Array>; pagination: { pageIndex: number; @@ -55,7 +55,7 @@ const emptyPrompt = ( export const AllRulesTablesComponent: React.FC = ({ euiBasicTableSelectionProps, - hasNoPermissions, + hasPermissions, monitoringColumns, pagination, rules, @@ -72,7 +72,7 @@ export const AllRulesTablesComponent: React.FC = ({ = ({ pagination={pagination} ref={tableRef} sorting={sorting} - selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} + selection={hasPermissions ? euiBasicTableSelectionProps : undefined} /> )} {selectedTab === AllRulesTabs.monitoring && ( void; loading: boolean; - userHasNoPermissions: boolean; + userHasPermissions: boolean; } const PrePackagedRulesPromptComponent: React.FC = ({ createPrePackagedRules, loading = false, - userHasNoPermissions = true, + userHasPermissions = false, }) => { const history = useHistory(); const handlePreBuiltCreation = useCallback(() => { @@ -64,16 +64,17 @@ const PrePackagedRulesPromptComponent: React.FC = ( const loadPrebuiltRulesAndTemplatesButton = useMemo( () => getLoadPrebuiltRulesAndTemplatesButton({ - isDisabled: userHasNoPermissions, + isDisabled: !userHasPermissions, onClick: handlePreBuiltCreation, fill: true, 'data-test-subj': 'load-prebuilt-rules', }), - [getLoadPrebuiltRulesAndTemplatesButton, handlePreBuiltCreation, userHasNoPermissions] + [getLoadPrebuiltRulesAndTemplatesButton, handlePreBuiltCreation, userHasPermissions] ); return ( {i18n.PRE_BUILT_TITLE}} body={

{i18n.PRE_BUILT_MSG}

} actions={ @@ -81,7 +82,7 @@ const PrePackagedRulesPromptComponent: React.FC = ( {loadPrebuiltRulesAndTemplatesButton} - `; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx index 53f478da28055f..3a27469ba25397 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx @@ -28,6 +28,16 @@ jest.mock('../../../pages/detection_engine/rules/all/actions', () => ({ editRuleAction: jest.fn(), })); +jest.mock('../../../../common/lib/kibana', () => { + return { + KibanaServices: { + get: () => ({ + http: { fetch: jest.fn() }, + }), + }, + }; +}); + const duplicateRulesActionMock = duplicateRulesAction as jest.Mock; const flushPromises = () => new Promise(setImmediate); @@ -41,7 +51,7 @@ describe('RuleActionsOverflow', () => { const wrapper = shallow( ); @@ -54,7 +64,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -70,11 +80,7 @@ describe('RuleActionsOverflow', () => { test('items are empty when there is a null rule within the rules-details-menu-panel', () => { const wrapper = mount( - + ); wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); wrapper.update(); @@ -85,11 +91,7 @@ describe('RuleActionsOverflow', () => { test('items are empty when there is an undefined rule within the rules-details-menu-panel', () => { const wrapper = mount( - + ); wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); wrapper.update(); @@ -102,7 +104,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -119,7 +121,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -137,7 +139,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -152,7 +154,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -167,7 +169,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -184,7 +186,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -198,11 +200,7 @@ describe('RuleActionsOverflow', () => { test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => { const rule = mockRule('id'); const wrapper = mount( - + ); wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); wrapper.update(); @@ -222,11 +220,7 @@ describe('RuleActionsOverflow', () => { const ruleDuplicate = mockRule('newRule'); duplicateRulesActionMock.mockImplementation(() => Promise.resolve([ruleDuplicate])); const wrapper = mount( - + ); wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); wrapper.update(); @@ -244,7 +238,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -259,7 +253,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -272,33 +266,11 @@ describe('RuleActionsOverflow', () => { ).toEqual(false); }); - test('it sets the rule.rule_id on the generic downloader when rules-details-export-rule is clicked', () => { - const rule = mockRule('id'); - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') - ).toEqual([rule.rule_id]); - }); - test('it does not close the pop over on rules-details-export-rule when the rule is an immutable rule and the user does a click', () => { const rule = mockRule('id'); rule.immutable = true; const wrapper = mount( - + ); wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); wrapper.update(); @@ -308,25 +280,6 @@ describe('RuleActionsOverflow', () => { wrapper.find('[data-test-subj="rules-details-popover"]').first().prop('isOpen') ).toEqual(true); }); - - test('it does not set the rule.rule_id on rules-details-export-rule when the rule is an immutable rule', () => { - const rule = mockRule('id'); - rule.immutable = true; - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') - ).toEqual([]); - }); }); describe('rules details delete rule', () => { @@ -335,7 +288,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -350,7 +303,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -367,7 +320,7 @@ describe('RuleActionsOverflow', () => { const wrapper = mount( ); @@ -381,11 +334,7 @@ describe('RuleActionsOverflow', () => { test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => { const rule = mockRule('id'); const wrapper = mount( - + ); wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index 0482e1997c9d13..e0841824d512f1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -12,23 +12,24 @@ import { EuiPopover, EuiToolTip, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import { useHistory } from 'react-router-dom'; -import { Rule, exportRules } from '../../../containers/detection_engine/rules'; +import { Rule } from '../../../containers/detection_engine/rules'; import * as i18n from './translations'; import * as i18nActions from '../../../pages/detection_engine/rules/translations'; -import { displaySuccessToast, useStateToaster } from '../../../../common/components/toasters'; +import { useStateToaster } from '../../../../common/components/toasters'; import { deleteRulesAction, duplicateRulesAction, editRuleAction, + exportRulesAction, } from '../../../pages/detection_engine/rules/all/actions'; -import { GenericDownloader } from '../../../../common/components/generic_downloader'; import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import { getToolTipContent } from '../../../../common/utils/privileges'; +import { useBoolState } from '../../../../common/hooks/use_bool_state'; const MyEuiButtonIcon = styled(EuiButtonIcon)` &.euiButtonIcon { @@ -43,7 +44,7 @@ const MyEuiButtonIcon = styled(EuiButtonIcon)` interface RuleActionsOverflowComponentProps { rule: Rule | null; - userHasNoPermissions: boolean; + userHasPermissions: boolean; canDuplicateRuleWithActions: boolean; } @@ -52,11 +53,10 @@ interface RuleActionsOverflowComponentProps { */ const RuleActionsOverflowComponent = ({ rule, - userHasNoPermissions, + userHasPermissions, canDuplicateRuleWithActions, }: RuleActionsOverflowComponentProps) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [rulesToExport, setRulesToExport] = useState([]); + const [isPopoverOpen, , closePopover, togglePopover] = useBoolState(); const history = useHistory(); const [, dispatchToaster] = useStateToaster(); @@ -71,10 +71,10 @@ const RuleActionsOverflowComponent = ({ { - setIsPopoverOpen(false); + closePopover(); const createdRules = await duplicateRulesAction( [rule], [rule.id], @@ -96,11 +96,11 @@ const RuleActionsOverflowComponent = ({ { - setIsPopoverOpen(false); - setRulesToExport([rule.rule_id]); + onClick={async () => { + closePopover(); + await exportRulesAction([rule.rule_id], noop, dispatchToaster); }} > {i18nActions.EXPORT_RULE} @@ -108,10 +108,10 @@ const RuleActionsOverflowComponent = ({ { - setIsPopoverOpen(false); + closePopover(); await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); }} > @@ -119,27 +119,30 @@ const RuleActionsOverflowComponent = ({ , ] : [], - // eslint-disable-next-line react-hooks/exhaustive-deps - [rule, userHasNoPermissions] + [ + canDuplicateRuleWithActions, + closePopover, + dispatchToaster, + history, + onRuleDeletedCallback, + rule, + userHasPermissions, + ] ); - const handlePopoverOpen = useCallback(() => { - setIsPopoverOpen(!isPopoverOpen); - }, [setIsPopoverOpen, isPopoverOpen]); - const button = useMemo( () => ( ), - [handlePopoverOpen, userHasNoPermissions] + [togglePopover, userHasPermissions] ); return ( @@ -147,7 +150,7 @@ const RuleActionsOverflowComponent = ({ setIsPopoverOpen(false)} + closePopover={closePopover} id="ruleActionsOverflow" isOpen={isPopoverOpen} data-test-subj="rules-details-popover" @@ -157,18 +160,6 @@ const RuleActionsOverflowComponent = ({ > - { - displaySuccessToast( - i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), - dispatchToaster - ); - }} - /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index d4c4e10813172a..7de91a07a68a0b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -13,6 +13,7 @@ import { DETECTION_ENGINE_RULES_STATUS_URL, DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, DETECTION_ENGINE_TAGS_URL, + DETECTION_ENGINE_RULES_BULK_ACTION, } from '../../../../../common/constants'; import { UpdateRulesProps, @@ -32,10 +33,14 @@ import { PrePackagedRulesStatusResponse, BulkRuleResponse, PatchRuleProps, + BulkActionProps, + BulkActionResponse, } from './types'; import { KibanaServices } from '../../../../common/lib/kibana'; import * as i18n from '../../../pages/detection_engine/rules/translations'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; +import { convertRulesFilterToKQL } from './utils'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas'; /** * Create provided Rule @@ -110,26 +115,7 @@ export const fetchRules = async ({ }, signal, }: FetchRulesProps): Promise => { - const showCustomRuleFilter = filterOptions.showCustomRules - ? [`alert.attributes.tags: "__internal_immutable:false"`] - : []; - const showElasticRuleFilter = filterOptions.showElasticRules - ? [`alert.attributes.tags: "__internal_immutable:true"`] - : []; - const filtersWithoutTags = [ - ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), - ...showCustomRuleFilter, - ...showElasticRuleFilter, - ].join(' AND '); - - const tags = filterOptions.tags - .map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) - .join(' AND '); - - const filterString = - filtersWithoutTags !== '' && tags !== '' - ? `${filtersWithoutTags} AND (${tags})` - : filtersWithoutTags + tags; + const filterString = convertRulesFilterToKQL(filterOptions); const getFieldNameForSortField = (field: string) => { return field === 'name' ? `${field}.keyword` : field; @@ -243,6 +229,23 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise({ + action, + query, +}: BulkActionProps): Promise> => + KibanaServices.get().http.fetch>(DETECTION_ENGINE_RULES_BULK_ACTION, { + method: 'POST', + body: JSON.stringify({ action, query }), + }); + /** * Create Prepackaged Rules * diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts index 60edeaf0de9832..2a983117db524a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.test.ts @@ -7,7 +7,7 @@ import { mockRule } from '../../../../pages/detection_engine/rules/all/__mocks__/mock'; import { FilterOptions, PaginationOptions } from '../types'; -import { RulesTableAction, RulesTableState, createRulesTableReducer } from './rules_table_reducer'; +import { RulesTableState, rulesTableReducer } from './rules_table_reducer'; const initialState: RulesTableState = { rules: [], @@ -24,10 +24,10 @@ const initialState: RulesTableState = { showCustomRules: false, showElasticRules: false, }, + isAllSelected: false, loadingRulesAction: null, loadingRuleIds: [], selectedRuleIds: [], - exportRuleIds: [], lastUpdated: 0, isRefreshOn: false, isRefreshing: false, @@ -35,36 +35,20 @@ const initialState: RulesTableState = { }; describe('allRulesReducer', () => { - let reducer: (state: RulesTableState, action: RulesTableAction) => RulesTableState; - beforeEach(() => { jest.useFakeTimers(); jest .spyOn(global.Date, 'now') .mockImplementationOnce(() => new Date('2020-10-31T11:01:58.135Z').valueOf()); - reducer = createRulesTableReducer({ current: null }); }); afterEach(() => { jest.clearAllMocks(); }); - describe('#exportRuleIds', () => { - test('should update state with rules to be exported', () => { - const { loadingRuleIds, loadingRulesAction, exportRuleIds } = reducer(initialState, { - type: 'exportRuleIds', - ids: ['123', '456'], - }); - - expect(loadingRuleIds).toEqual(['123', '456']); - expect(exportRuleIds).toEqual(['123', '456']); - expect(loadingRulesAction).toEqual('export'); - }); - }); - describe('#loadingRuleIds', () => { - test('should update state with rule ids with a pending action', () => { - const { loadingRuleIds, loadingRulesAction } = reducer(initialState, { + it('should update state with rule ids with a pending action', () => { + const { loadingRuleIds, loadingRulesAction } = rulesTableReducer(initialState, { type: 'loadingRuleIds', ids: ['123', '456'], actionType: 'enable', @@ -74,8 +58,8 @@ describe('allRulesReducer', () => { expect(loadingRulesAction).toEqual('enable'); }); - test('should update loadingIds to empty array if action is null', () => { - const { loadingRuleIds, loadingRulesAction } = reducer(initialState, { + it('should update loadingIds to empty array if action is null', () => { + const { loadingRuleIds, loadingRulesAction } = rulesTableReducer(initialState, { type: 'loadingRuleIds', ids: ['123', '456'], actionType: null, @@ -85,8 +69,8 @@ describe('allRulesReducer', () => { expect(loadingRulesAction).toBeNull(); }); - test('should append rule ids to any existing loading ids', () => { - const { loadingRuleIds, loadingRulesAction } = reducer( + it('should append rule ids to any existing loading ids', () => { + const { loadingRuleIds, loadingRulesAction } = rulesTableReducer( { ...initialState, loadingRuleIds: ['abc'] }, { type: 'loadingRuleIds', @@ -101,8 +85,8 @@ describe('allRulesReducer', () => { }); describe('#selectedRuleIds', () => { - test('should update state with selected rule ids', () => { - const { selectedRuleIds } = reducer(initialState, { + it('should update state with selected rule ids', () => { + const { selectedRuleIds } = rulesTableReducer(initialState, { type: 'selectedRuleIds', ids: ['123', '456'], }); @@ -112,19 +96,22 @@ describe('allRulesReducer', () => { }); describe('#setRules', () => { - test('should update rules and reset loading/selected rule ids', () => { - const { selectedRuleIds, loadingRuleIds, loadingRulesAction, pagination, rules } = reducer( - initialState, - { - type: 'setRules', - rules: [mockRule('someRuleId')], - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - } - ); + it('should update rules and reset loading/selected rule ids', () => { + const { + selectedRuleIds, + loadingRuleIds, + loadingRulesAction, + pagination, + rules, + } = rulesTableReducer(initialState, { + type: 'setRules', + rules: [mockRule('someRuleId')], + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + }); expect(rules).toEqual([mockRule('someRuleId')]); expect(selectedRuleIds).toEqual([]); @@ -139,9 +126,9 @@ describe('allRulesReducer', () => { }); describe('#updateRules', () => { - test('should return existing and new rules', () => { + it('should return existing and new rules', () => { const existingRule = { ...mockRule('123'), rule_id: 'rule-123' }; - const { rules, loadingRulesAction } = reducer( + const { rules, loadingRulesAction } = rulesTableReducer( { ...initialState, rules: [existingRule] }, { type: 'updateRules', @@ -153,9 +140,9 @@ describe('allRulesReducer', () => { expect(loadingRulesAction).toBeNull(); }); - test('should return updated rule', () => { + it('should return updated rule', () => { const updatedRule = { ...mockRule('someRuleId'), description: 'updated rule' }; - const { rules, loadingRulesAction } = reducer( + const { rules, loadingRulesAction } = rulesTableReducer( { ...initialState, rules: [mockRule('someRuleId')] }, { type: 'updateRules', @@ -167,9 +154,9 @@ describe('allRulesReducer', () => { expect(loadingRulesAction).toBeNull(); }); - test('should return updated existing loading rule ids', () => { + it('should return updated existing loading rule ids', () => { const existingRule = { ...mockRule('someRuleId'), id: '123', rule_id: 'rule-123' }; - const { loadingRuleIds, loadingRulesAction } = reducer( + const { loadingRuleIds, loadingRulesAction } = rulesTableReducer( { ...initialState, rules: [existingRule], @@ -188,7 +175,7 @@ describe('allRulesReducer', () => { }); describe('#updateFilterOptions', () => { - test('should return existing and new rules', () => { + it('should return existing and new rules', () => { const paginationMock: PaginationOptions = { page: 1, perPage: 20, @@ -202,7 +189,7 @@ describe('allRulesReducer', () => { showCustomRules: false, showElasticRules: false, }; - const { filterOptions, pagination } = reducer(initialState, { + const { filterOptions, pagination } = rulesTableReducer(initialState, { type: 'updateFilterOptions', filterOptions: filterMock, pagination: paginationMock, @@ -214,8 +201,8 @@ describe('allRulesReducer', () => { }); describe('#failure', () => { - test('should reset rules value to empty array', () => { - const { rules } = reducer(initialState, { + it('should reset rules value to empty array', () => { + const { rules } = rulesTableReducer(initialState, { type: 'failure', }); @@ -224,8 +211,8 @@ describe('allRulesReducer', () => { }); describe('#setLastRefreshDate', () => { - test('should update last refresh date with current date', () => { - const { lastUpdated } = reducer(initialState, { + it('should update last refresh date with current date', () => { + const { lastUpdated } = rulesTableReducer(initialState, { type: 'setLastRefreshDate', }); @@ -234,8 +221,8 @@ describe('allRulesReducer', () => { }); describe('#setShowIdleModal', () => { - test('should hide idle modal and restart refresh if "show" is false', () => { - const { showIdleModal, isRefreshOn } = reducer(initialState, { + it('should hide idle modal and restart refresh if "show" is false', () => { + const { showIdleModal, isRefreshOn } = rulesTableReducer(initialState, { type: 'setShowIdleModal', show: false, }); @@ -244,8 +231,8 @@ describe('allRulesReducer', () => { expect(isRefreshOn).toBeTruthy(); }); - test('should show idle modal and pause refresh if "show" is true', () => { - const { showIdleModal, isRefreshOn } = reducer(initialState, { + it('should show idle modal and pause refresh if "show" is true', () => { + const { showIdleModal, isRefreshOn } = rulesTableReducer(initialState, { type: 'setShowIdleModal', show: true, }); @@ -256,8 +243,8 @@ describe('allRulesReducer', () => { }); describe('#setAutoRefreshOn', () => { - test('should pause auto refresh if "paused" is true', () => { - const { isRefreshOn } = reducer(initialState, { + it('should pause auto refresh if "paused" is true', () => { + const { isRefreshOn } = rulesTableReducer(initialState, { type: 'setAutoRefreshOn', on: true, }); @@ -265,8 +252,8 @@ describe('allRulesReducer', () => { expect(isRefreshOn).toBeTruthy(); }); - test('should resume auto refresh if "paused" is false', () => { - const { isRefreshOn } = reducer(initialState, { + it('should resume auto refresh if "paused" is false', () => { + const { isRefreshOn } = rulesTableReducer(initialState, { type: 'setAutoRefreshOn', on: false, }); @@ -274,4 +261,58 @@ describe('allRulesReducer', () => { expect(isRefreshOn).toBeFalsy(); }); }); + + describe('#selectAllRules', () => { + it('should select all rules', () => { + const state = rulesTableReducer( + { + ...initialState, + rules: [mockRule('1'), mockRule('2'), mockRule('3')], + }, + { + type: 'setIsAllSelected', + isAllSelected: true, + } + ); + + expect(state.isAllSelected).toBe(true); + expect(state.selectedRuleIds).toEqual(['1', '2', '3']); + }); + + it('should deselect all rules', () => { + const state = rulesTableReducer( + { + ...initialState, + rules: [mockRule('1'), mockRule('2'), mockRule('3')], + isAllSelected: true, + selectedRuleIds: ['1', '2', '3'], + }, + { + type: 'setIsAllSelected', + isAllSelected: false, + } + ); + + expect(state.isAllSelected).toBe(false); + expect(state.selectedRuleIds).toEqual([]); + }); + + it('should unset "isAllSelected" on selected rules modification', () => { + const state = rulesTableReducer( + { + ...initialState, + rules: [mockRule('1'), mockRule('2'), mockRule('3')], + isAllSelected: true, + selectedRuleIds: ['1', '2', '3'], + }, + { + type: 'selectedRuleIds', + ids: ['1', '2'], + } + ); + + expect(state.isAllSelected).toBe(false); + expect(state.selectedRuleIds).toEqual(['1', '2']); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts index 01a87fef2b723d..7d32785222fed5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_reducer.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type React from 'react'; -import { EuiBasicTable } from '@elastic/eui'; import { FilterOptions, PaginationOptions, Rule } from '../types'; export type LoadingRuleAction = @@ -25,11 +23,11 @@ export interface RulesTableState { loadingRulesAction: LoadingRuleAction; loadingRuleIds: string[]; selectedRuleIds: string[]; - exportRuleIds: string[]; lastUpdated: number; isRefreshOn: boolean; isRefreshing: boolean; showIdleModal: boolean; + isAllSelected: boolean; } export type RulesTableAction = @@ -42,128 +40,119 @@ export type RulesTableAction = } | { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction } | { type: 'selectedRuleIds'; ids: string[] } - | { type: 'exportRuleIds'; ids: string[] } | { type: 'setLastRefreshDate' } | { type: 'setAutoRefreshOn'; on: boolean } | { type: 'setIsRefreshing'; isRefreshing: boolean } + | { type: 'setIsAllSelected'; isAllSelected: boolean } | { type: 'setShowIdleModal'; show: boolean } | { type: 'failure' }; -export const createRulesTableReducer = ( - tableRef: React.MutableRefObject | null> -) => { - const rulesTableReducer = (state: RulesTableState, action: RulesTableAction): RulesTableState => { - switch (action.type) { - case 'setRules': { - if (tableRef?.current?.changeSelection != null) { - // for future devs: eui basic table is not giving us a prop to set the value, so - // we are using the ref in setTimeout to reset on the next loop so that we - // do not get a warning telling us we are trying to update during a render - window.setTimeout(() => tableRef?.current?.changeSelection([]), 0); +export const rulesTableReducer = ( + state: RulesTableState, + action: RulesTableAction +): RulesTableState => { + switch (action.type) { + case 'setRules': { + return { + ...state, + rules: action.rules, + selectedRuleIds: state.isAllSelected ? action.rules.map(({ id }) => id) : [], + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + ...state.pagination, + ...action.pagination, + }, + }; + } + case 'updateRules': { + const ruleIds = state.rules.map((r) => r.id); + const updatedRules = action.rules.reduce((rules, updatedRule) => { + let newRules = rules; + if (ruleIds.includes(updatedRule.id)) { + newRules = newRules.map((r) => (updatedRule.id === r.id ? updatedRule : r)); + } else { + newRules = [...newRules, updatedRule]; } - - return { - ...state, - rules: action.rules, - selectedRuleIds: [], - loadingRuleIds: [], - loadingRulesAction: null, - pagination: { - ...state.pagination, - ...action.pagination, - }, - }; - } - case 'updateRules': { - const ruleIds = state.rules.map((r) => r.id); - const updatedRules = action.rules.reduce((rules, updatedRule) => { - let newRules = rules; - if (ruleIds.includes(updatedRule.id)) { - newRules = newRules.map((r) => (updatedRule.id === r.id ? updatedRule : r)); - } else { - newRules = [...newRules, updatedRule]; - } - return newRules; - }, state.rules); - const updatedRuleIds = action.rules.map((r) => r.id); - const newLoadingRuleIds = state.loadingRuleIds.filter((id) => !updatedRuleIds.includes(id)); - return { - ...state, - rules: updatedRules, - loadingRuleIds: newLoadingRuleIds, - loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction, - }; - } - case 'updateFilterOptions': { - return { - ...state, - filterOptions: { - ...state.filterOptions, - ...action.filterOptions, - }, - pagination: { - ...state.pagination, - ...action.pagination, - }, - }; - } - case 'loadingRuleIds': { - return { - ...state, - loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids], - loadingRulesAction: action.actionType, - }; - } - case 'selectedRuleIds': { - return { - ...state, - selectedRuleIds: action.ids, - }; - } - case 'exportRuleIds': { - return { - ...state, - loadingRuleIds: action.ids, - loadingRulesAction: 'export', - exportRuleIds: action.ids, - }; - } - case 'setLastRefreshDate': { - return { - ...state, - lastUpdated: Date.now(), - }; - } - case 'setAutoRefreshOn': { - return { - ...state, - isRefreshOn: action.on, - }; - } - case 'setIsRefreshing': { - return { - ...state, - isRefreshing: action.isRefreshing, - }; - } - case 'setShowIdleModal': { - return { - ...state, - showIdleModal: action.show, - isRefreshOn: !action.show, - }; - } - case 'failure': { - return { - ...state, - rules: [], - }; - } - default: { - return state; - } + return newRules; + }, state.rules); + const updatedRuleIds = action.rules.map((r) => r.id); + const newLoadingRuleIds = state.loadingRuleIds.filter((id) => !updatedRuleIds.includes(id)); + return { + ...state, + rules: updatedRules, + loadingRuleIds: newLoadingRuleIds, + loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction, + }; } - }; - - return rulesTableReducer; + case 'updateFilterOptions': { + return { + ...state, + filterOptions: { + ...state.filterOptions, + ...action.filterOptions, + }, + pagination: { + ...state.pagination, + ...action.pagination, + }, + }; + } + case 'loadingRuleIds': { + return { + ...state, + loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids], + loadingRulesAction: action.actionType, + }; + } + case 'selectedRuleIds': { + return { + ...state, + isAllSelected: false, + selectedRuleIds: action.ids, + }; + } + case 'setLastRefreshDate': { + return { + ...state, + lastUpdated: Date.now(), + }; + } + case 'setAutoRefreshOn': { + return { + ...state, + isRefreshOn: action.on, + }; + } + case 'setIsRefreshing': { + return { + ...state, + isRefreshing: action.isRefreshing, + }; + } + case 'setIsAllSelected': { + const { isAllSelected } = action; + return { + ...state, + isAllSelected, + selectedRuleIds: isAllSelected ? state.rules.map(({ id }) => id) : [], + }; + } + case 'setShowIdleModal': { + return { + ...state, + showIdleModal: action.show, + isRefreshOn: !action.show, + }; + } + case 'failure': { + return { + ...state, + rules: [], + }; + } + default: { + return state; + } + } }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts new file mode 100644 index 00000000000000..cce45f87d8ce38 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_async_confirmation.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useRef } from 'react'; + +type UseAsyncConfirmationReturn = [ + initConfirmation: () => Promise, + confirm: () => void, + cancel: () => void +]; + +interface UseAsyncConfirmationArgs { + onInit: () => void; + onFinish: () => void; +} + +export const useAsyncConfirmation = ({ + onInit, + onFinish, +}: UseAsyncConfirmationArgs): UseAsyncConfirmationReturn => { + const confirmationPromiseRef = useRef<(result: boolean) => void>(); + + const confirm = useCallback(() => { + confirmationPromiseRef.current?.(true); + }, []); + + const cancel = useCallback(() => { + confirmationPromiseRef.current?.(false); + }, []); + + const initConfirmation = useCallback(() => { + onInit(); + + return new Promise((resolve) => { + confirmationPromiseRef.current = resolve; + }).finally(() => { + onFinish(); + }); + }, [onInit, onFinish]); + + return [initConfirmation, confirm, cancel]; +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts index 8969843f61a1cd..cb41401ee2f40a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/use_rules_table.ts @@ -5,14 +5,11 @@ * 2.0. */ -import { Dispatch, useMemo, useReducer, useEffect, useRef } from 'react'; -import { EuiBasicTable } from '@elastic/eui'; - +import { Dispatch, useReducer, useEffect, useRef } from 'react'; import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import * as i18n from '../translations'; - import { fetchRules } from '../api'; -import { createRulesTableReducer, RulesTableState, RulesTableAction } from './rules_table_reducer'; +import { rulesTableReducer, RulesTableState, RulesTableAction } from './rules_table_reducer'; import { createRulesTableFacade, RulesTableFacade } from './rules_table_facade'; const INITIAL_SORT_FIELD = 'enabled'; @@ -35,15 +32,14 @@ const initialStateDefaults: RulesTableState = { loadingRulesAction: null, loadingRuleIds: [], selectedRuleIds: [], - exportRuleIds: [], lastUpdated: 0, isRefreshOn: true, isRefreshing: false, + isAllSelected: false, showIdleModal: false, }; export interface UseRulesTableParams { - tableRef: React.MutableRefObject | null>; initialStateOverride?: Partial; } @@ -54,7 +50,7 @@ export interface UseRulesTableReturn extends RulesTableFacade { } export const useRulesTable = (params: UseRulesTableParams): UseRulesTableReturn => { - const { tableRef, initialStateOverride } = params; + const { initialStateOverride } = params; const initialState: RulesTableState = { ...initialStateDefaults, @@ -62,8 +58,7 @@ export const useRulesTable = (params: UseRulesTableParams): UseRulesTableReturn ...initialStateOverride, }; - const reducer = useMemo(() => createRulesTableReducer(tableRef), [tableRef]); - const [state, dispatch] = useReducer(reducer, initialState); + const [state, dispatch] = useReducer(rulesTableReducer, initialState); const facade = useRef(createRulesTableFacade(dispatch)); const { addError } = useAppToasts(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 85ff0f9ac14579..20bdeaf7e63787 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -28,6 +28,7 @@ import { rule_name_override, timestamp_override, threshold, + BulkAction, } from '../../../../../common/detection_engine/schemas/common/schemas'; import { CreateRulesSchema, @@ -212,6 +213,24 @@ export interface DuplicateRulesProps { rules: Rule[]; } +export interface BulkActionProps { + action: Action; + query: string; +} + +export interface BulkActionResult { + success: boolean; + rules_count: number; +} + +export type BulkActionResponse = { + [BulkAction.delete]: BulkActionResult; + [BulkAction.disable]: BulkActionResult; + [BulkAction.enable]: BulkActionResult; + [BulkAction.duplicate]: BulkActionResult; + [BulkAction.export]: Blob; +}[Action]; + export interface BasicFetchProps { signal: AbortSignal; } @@ -248,7 +267,7 @@ export interface ExportDocumentsProps { ids: string[]; filename?: string; excludeExportDetails?: boolean; - signal: AbortSignal; + signal?: AbortSignal; } export interface RuleStatus { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts new file mode 100644 index 00000000000000..c293e26f1740c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { INTERNAL_IMMUTABLE_KEY } from '../../../../../common/constants'; +import { FilterOptions } from './types'; +import { convertRulesFilterToKQL } from './utils'; + +describe('convertRulesFilterToKQL', () => { + const filterOptions: FilterOptions = { + filter: '', + sortField: 'name', + sortOrder: 'asc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }; + + it('returns empty string if filter options are empty', () => { + const kql = convertRulesFilterToKQL(filterOptions); + + expect(kql).toBe(''); + }); + + it('handles presence of "filter" properly', () => { + const kql = convertRulesFilterToKQL({ ...filterOptions, filter: 'foo' }); + + expect(kql).toBe('alert.attributes.name: foo'); + }); + + it('handles presence of "showCustomRules" properly', () => { + const kql = convertRulesFilterToKQL({ ...filterOptions, showCustomRules: true }); + + expect(kql).toBe(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`); + }); + + it('handles presence of "showElasticRules" properly', () => { + const kql = convertRulesFilterToKQL({ ...filterOptions, showElasticRules: true }); + + expect(kql).toBe(`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`); + }); + + it('handles presence of "tags" properly', () => { + const kql = convertRulesFilterToKQL({ ...filterOptions, tags: ['tag1', 'tag2'] }); + + expect(kql).toBe('alert.attributes.tags: "tag1" AND alert.attributes.tags: "tag2"'); + }); + + it('handles combination of different properties properly', () => { + const kql = convertRulesFilterToKQL({ + ...filterOptions, + filter: 'foo', + showElasticRules: true, + tags: ['tag1', 'tag2'], + }); + + expect(kql).toBe( + `alert.attributes.name: foo AND alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true" AND (alert.attributes.tags: "tag1" AND alert.attributes.tags: "tag2")` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.ts new file mode 100644 index 00000000000000..841b2adca09e04 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/utils.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. + */ + +import { INTERNAL_IMMUTABLE_KEY } from '../../../../../common/constants'; +import { FilterOptions } from './types'; + +/** + * Convert rules filter options object to KQL query + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * + * @returns KQL string + */ +export const convertRulesFilterToKQL = (filterOptions: FilterOptions): string => { + const showCustomRuleFilter = filterOptions.showCustomRules + ? [`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`] + : []; + const showElasticRuleFilter = filterOptions.showElasticRules + ? [`alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`] + : []; + const filtersWithoutTags = [ + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), + ...showCustomRuleFilter, + ...showElasticRuleFilter, + ].join(' AND '); + + const tags = filterOptions.tags + .map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) + .join(' AND '); + + const filterString = + filtersWithoutTags !== '' && tags !== '' + ? `${filtersWithoutTags} AND (${tags})` + : filtersWithoutTags + tags; + + return filterString; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx index de33d414398a87..78fac10815d455 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx @@ -7,29 +7,29 @@ import * as H from 'history'; import React, { Dispatch } from 'react'; - +import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas'; import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; -import { - deleteRules, - duplicateRules, - enableRules, - Rule, - RulesTableAction, -} from '../../../../containers/detection_engine/rules'; - import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; - import { ActionToaster, displayErrorToast, displaySuccessToast, errorToToaster, } from '../../../../../common/components/toasters'; -import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../../../common/lib/telemetry'; - -import * as i18n from '../translations'; -import { bucketRulesResponse } from './helpers'; +import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../../../common/lib/telemetry'; +import { downloadBlob } from '../../../../../common/utils/download_blob'; +import { + deleteRules, + duplicateRules, + enableRules, + exportRules, + performBulkAction, + Rule, + RulesTableAction, +} from '../../../../containers/detection_engine/rules'; import { transformOutput } from '../../../../containers/detection_engine/rules/transforms'; +import * as i18n from '../translations'; +import { bucketRulesResponse, getExportedRulesCount } from './helpers'; export const editRuleAction = (rule: Rule, history: H.History) => { history.push(getEditRuleUrl(rule.id)); @@ -58,20 +58,34 @@ export const duplicateRulesAction = async ( } else { displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); } - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - return createdRules; } catch (error) { - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); errorToToaster({ title: i18n.DUPLICATE_RULE_ERROR, error, dispatchToaster }); + } finally { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } }; -export const exportRulesAction = ( +export const exportRulesAction = async ( exportRuleId: string[], - dispatch: React.Dispatch + dispatch: React.Dispatch, + dispatchToaster: Dispatch ) => { - dispatch({ type: 'exportRuleIds', ids: exportRuleId }); + try { + dispatch({ type: 'loadingRuleIds', ids: exportRuleId, actionType: 'export' }); + const blob = await exportRules({ ids: exportRuleId }); + downloadBlob(blob, `${i18n.EXPORT_FILENAME}.ndjson`); + + const exportedRulesCount = await getExportedRulesCount(blob); + displaySuccessToast( + i18n.SUCCESSFULLY_EXPORTED_RULES(exportedRulesCount, exportRuleId.length), + dispatchToaster + ); + } catch (e) { + displayErrorToast(i18n.BULK_ACTION_FAILED, [e.message], dispatchToaster); + } finally { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + } }; export const deleteRulesAction = async ( @@ -84,7 +98,6 @@ export const deleteRulesAction = async ( dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'delete' }); const response = await deleteRules({ ids: ruleIds }); const { errors } = bucketRulesResponse(response); - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); if (errors.length > 0) { displayErrorToast( i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), @@ -95,12 +108,13 @@ export const deleteRulesAction = async ( onRuleDeleted(); } } catch (error) { - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); errorToToaster({ title: i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), error, dispatchToaster, }); + } finally { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } }; @@ -144,6 +158,37 @@ export const enableRulesAction = async ( } } catch (e) { displayErrorToast(errorTitle, [e.message], dispatchToaster); + } finally { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + } +}; + +export const rulesBulkActionByQuery = async ( + visibleRuleIds: string[], + selectedItemsCount: number, + query: string, + action: BulkAction, + dispatch: React.Dispatch, + dispatchToaster: Dispatch +) => { + try { + dispatch({ type: 'loadingRuleIds', ids: visibleRuleIds, actionType: action }); + + if (action === BulkAction.export) { + const blob = await performBulkAction({ query, action }); + downloadBlob(blob, `${i18n.EXPORT_FILENAME}.ndjson`); + + const exportedRulesCount = await getExportedRulesCount(blob); + displaySuccessToast( + i18n.SUCCESSFULLY_EXPORTED_RULES(exportedRulesCount, selectedItemsCount), + dispatchToaster + ); + } else { + await performBulkAction({ query, action }); + } + } catch (e) { + displayErrorToast(i18n.BULK_ACTION_FAILED, [e.message], dispatchToaster); + } finally { dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); } }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx index 648d653d6a3c84..5b558824b4659a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/batch_actions.tsx @@ -10,6 +10,7 @@ import React, { Dispatch } from 'react'; import * as i18n from '../translations'; import { RulesTableAction } from '../../../../containers/detection_engine/rules/rules_table'; import { + rulesBulkActionByQuery, deleteRulesAction, duplicateRulesAction, enableRulesAction, @@ -20,6 +21,7 @@ import { Rule } from '../../../../containers/detection_engine/rules'; import * as detectionI18n from '../../translations'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { canEditRuleWithActions } from '../../../../../common/utils/privileges'; +import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas'; interface GetBatchItems { closePopover: () => void; @@ -32,6 +34,10 @@ interface GetBatchItems { refetchPrePackagedRulesStatus: () => Promise; rules: Rule[]; selectedRuleIds: string[]; + isAllSelected: boolean; + filterQuery: string; + confirmDeletion: () => Promise; + selectedItemsCount: number; } export const getBatchItems = ({ @@ -45,51 +51,138 @@ export const getBatchItems = ({ rules, selectedRuleIds, hasActionsPrivileges, + isAllSelected, + filterQuery, + confirmDeletion, + selectedItemsCount, }: GetBatchItems) => { - const selectedRules = selectedRuleIds.reduce>((acc, id) => { - const found = rules.find((r) => r.id === id); - if (found != null) { - return { [id]: found, ...acc }; - } - return acc; - }, {}); + const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id)); - const containsEnabled = selectedRuleIds.some((id) => selectedRules[id]?.enabled ?? false); - const containsDisabled = selectedRuleIds.some((id) => !selectedRules[id]?.enabled ?? false); + const containsEnabled = selectedRules.some(({ enabled }) => enabled); + const containsDisabled = selectedRules.some(({ enabled }) => !enabled); const containsLoading = selectedRuleIds.some((id) => loadingRuleIds.includes(id)); - const containsImmutable = selectedRuleIds.some((id) => selectedRules[id]?.immutable ?? false); + const containsImmutable = selectedRules.some(({ immutable }) => immutable); const missingActionPrivileges = !hasActionsPrivileges && - selectedRuleIds.some((id) => { - return !canEditRuleWithActions(selectedRules[id], hasActionsPrivileges); - }); + selectedRules.some((rule) => !canEditRuleWithActions(rule, hasActionsPrivileges)); + + const handleActivateAction = async () => { + closePopover(); + const deactivatedRules = selectedRules.filter(({ enabled }) => !enabled); + const deactivatedRulesNoML = deactivatedRules.filter(({ type }) => !isMlRule(type)); + + const mlRuleCount = deactivatedRules.length - deactivatedRulesNoML.length; + if (!hasMlPermissions && mlRuleCount > 0) { + displayWarningToast(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount), dispatchToaster); + } + + const ruleIds = hasMlPermissions + ? deactivatedRules.map(({ id }) => id) + : deactivatedRulesNoML.map(({ id }) => id); + + if (isAllSelected) { + await rulesBulkActionByQuery( + ruleIds, + selectedItemsCount, + filterQuery, + BulkAction.enable, + dispatch, + dispatchToaster + ); + await reFetchRules(); + } else { + await enableRulesAction(ruleIds, true, dispatch, dispatchToaster); + } + }; + + const handleDeactivateActions = async () => { + closePopover(); + const activatedIds = selectedRules.filter(({ enabled }) => enabled).map(({ id }) => id); + if (isAllSelected) { + await rulesBulkActionByQuery( + activatedIds, + selectedItemsCount, + filterQuery, + BulkAction.disable, + dispatch, + dispatchToaster + ); + await reFetchRules(); + } else { + await enableRulesAction(activatedIds, false, dispatch, dispatchToaster); + } + }; + + const handleDuplicateAction = async () => { + closePopover(); + if (isAllSelected) { + await rulesBulkActionByQuery( + selectedRuleIds, + selectedItemsCount, + filterQuery, + BulkAction.duplicate, + dispatch, + dispatchToaster + ); + await reFetchRules(); + } else { + await duplicateRulesAction(selectedRules, selectedRuleIds, dispatch, dispatchToaster); + } + await reFetchRules(); + await refetchPrePackagedRulesStatus(); + }; + + const handleDeleteAction = async () => { + closePopover(); + if (isAllSelected) { + if ((await confirmDeletion()) === false) { + // User has cancelled deletion + return; + } + + await rulesBulkActionByQuery( + selectedRuleIds, + selectedItemsCount, + filterQuery, + BulkAction.delete, + dispatch, + dispatchToaster + ); + } else { + await deleteRulesAction(selectedRuleIds, dispatch, dispatchToaster); + } + await reFetchRules(); + await refetchPrePackagedRulesStatus(); + }; + + const handleExportAction = async () => { + closePopover(); + if (isAllSelected) { + await rulesBulkActionByQuery( + selectedRuleIds, + selectedItemsCount, + filterQuery, + BulkAction.export, + dispatch, + dispatchToaster + ); + } else { + await exportRulesAction( + selectedRules.map((r) => r.rule_id), + dispatch, + dispatchToaster + ); + } + }; return [ { - closePopover(); - const deactivatedIds = selectedRuleIds.filter((id) => !selectedRules[id]?.enabled ?? false); - - const deactivatedIdsNoML = deactivatedIds.filter( - (id) => !isMlRule(selectedRules[id]?.type) - ); - - const mlRuleCount = deactivatedIds.length - deactivatedIdsNoML.length; - if (!hasMlPermissions && mlRuleCount > 0) { - displayWarningToast(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount), dispatchToaster); - } - - await enableRulesAction( - hasMlPermissions ? deactivatedIds : deactivatedIdsNoML, - true, - dispatch, - dispatchToaster - ); - }} + disabled={missingActionPrivileges || containsLoading || (!containsDisabled && !isAllSelected)} + onClick={handleActivateAction} > , { - closePopover(); - const activatedIds = selectedRuleIds.filter((id) => selectedRules[id]?.enabled ?? false); - await enableRulesAction(activatedIds, false, dispatch, dispatchToaster); - }} + disabled={missingActionPrivileges || containsLoading || (!containsEnabled && !isAllSelected)} + onClick={handleDeactivateActions} > , { - closePopover(); - exportRulesAction( - rules.filter((r) => selectedRuleIds.includes(r.id)).map((r) => r.rule_id), - dispatch - ); - }} + disabled={ + (containsImmutable && !isAllSelected) || containsLoading || selectedRuleIds.length === 0 + } + onClick={handleExportAction} > {i18n.BATCH_ACTION_EXPORT_SELECTED} , @@ -135,17 +222,7 @@ export const getBatchItems = ({ data-test-subj="duplicateRuleBulk" icon="copy" disabled={missingActionPrivileges || containsLoading || selectedRuleIds.length === 0} - onClick={async () => { - closePopover(); - await duplicateRulesAction( - rules.filter((r) => selectedRuleIds.includes(r.id)), - selectedRuleIds, - dispatch, - dispatchToaster - ); - await reFetchRules(); - await refetchPrePackagedRulesStatus(); - }} + onClick={handleDuplicateAction} > { - closePopover(); - await deleteRulesAction(selectedRuleIds, dispatch, dispatchToaster); - await reFetchRules(); - await refetchPrePackagedRulesStatus(); - }} + onClick={handleDeleteAction} > {i18n.BATCH_ACTION_DELETE_SELECTED} , diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 83bb530827fa2a..28a65c3e64e1f8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -96,7 +96,7 @@ export const getActions = ( description: i18n.EXPORT_RULE, icon: 'exportAction', name: i18n.EXPORT_RULE, - onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), + onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch, dispatchToaster), enabled: (rowItem: Rule) => !rowItem.immutable, }, { @@ -125,7 +125,7 @@ interface GetColumns { formatUrl: FormatUrl; history: H.History; hasMlPermissions: boolean; - hasNoPermissions: boolean; + hasPermissions: boolean; loadingRuleIds: string[]; reFetchRules: () => Promise; refetchPrePackagedRulesStatus: () => Promise; @@ -142,7 +142,7 @@ export const getColumns = ({ formatUrl, history, hasMlPermissions, - hasNoPermissions, + hasPermissions, loadingRuleIds, reFetchRules, refetchPrePackagedRulesStatus, @@ -275,7 +275,7 @@ export const getColumns = ({ enabled={item.enabled} isDisabled={ !canEditRuleWithActions(item, hasReadActionsPrivileges) || - hasNoPermissions || + !hasPermissions || (isMlRule(item.type) && !hasMlPermissions && !item.enabled) } isLoading={loadingRuleIds.includes(item.id)} @@ -300,7 +300,7 @@ export const getColumns = ({ } as EuiTableActionsColumnType, ]; - return hasNoPermissions ? cols : [...cols, ...actions]; + return hasPermissions ? [...cols, ...actions] : cols; }; export const getMonitoringColumns = ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx index 6a0f4dc4e2dea0..dd3549ea20d365 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -79,7 +79,7 @@ describe('ExceptionListsTable', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 1dfa83da1637a7..7f734b10fd0200 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -38,7 +38,7 @@ export type Func = () => Promise; interface ExceptionListsTableProps { history: History; - hasNoPermissions: boolean; + hasPermissions: boolean; loading: boolean; formatUrl: FormatUrl; } @@ -60,7 +60,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = { }; export const ExceptionListsTable = React.memo( - ({ formatUrl, history, hasNoPermissions, loading }) => { + ({ formatUrl, history, hasPermissions, loading }) => { const { services: { http, notifications }, } = useKibana(); @@ -359,7 +359,7 @@ export const ExceptionListsTable = React.memo( <> ( => { + const blobContent = await blob.text(); + // The Blob content is an NDJSON file, the last line of which contains export details. + const exportDetailsJson = blobContent.split('\n').filter(Boolean).slice(-1)[0]; + const exportDetails = JSON.parse(exportDetailsJson); + + return exportDetails.exported_count; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 1f4586754cb337..9597c221843be7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -124,10 +124,10 @@ describe('AllRules', () => { loadingRulesAction: null, loadingRuleIds: [], selectedRuleIds: [], - exportRuleIds: [], lastUpdated: 0, isRefreshOn: true, isRefreshing: false, + isAllSelected: false, showIdleModal: false, }; @@ -189,7 +189,7 @@ describe('AllRules', () => { const wrapper = shallow( { { { { { Promise; @@ -64,7 +64,7 @@ const allRulesTabs = [ export const AllRules = React.memo( ({ createPrePackagedRules, - hasNoPermissions, + hasPermissions, loading, loadingCreatePrePackagedRules, refetchPrePackagedRulesStatus, @@ -110,7 +110,7 @@ export const AllRules = React.memo( formatUrl={formatUrl} selectedTab={allRulesTab} createPrePackagedRules={createPrePackagedRules} - hasNoPermissions={hasNoPermissions} + hasPermissions={hasPermissions} loading={loading} loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} refetchPrePackagedRulesStatus={refetchPrePackagedRulesStatus} @@ -125,7 +125,7 @@ export const AllRules = React.memo( )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 353cc657f21167..8fd82a495e52f8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -15,7 +15,6 @@ import { EuiWindowEvent, } from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import uuid from 'uuid'; import { debounce } from 'lodash/fp'; import { History } from 'history'; @@ -25,7 +24,6 @@ import { CreatePreBuiltRules, FilterOptions, Rule, - exportRules, RulesSortingFields, } from '../../../../containers/detection_engine/rules'; @@ -36,7 +34,6 @@ import { useStateToaster } from '../../../../../common/components/toasters'; import { Loader } from '../../../../../common/components/loader'; import { Panel } from '../../../../../common/components/panel'; import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt'; -import { GenericDownloader } from '../../../../../common/components/generic_downloader'; import { AllRulesTables, SortingType } from '../../../../components/rules/all_rules_tables'; import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; @@ -53,6 +50,10 @@ import { AllRulesUtilityBar } from './utility_bar'; import { LastUpdatedAt } from '../../../../../common/components/last_updated'; import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants'; import { AllRulesTabs } from '.'; +import { useValueChanged } from '../../../../../common/hooks/use_value_changed'; +import { convertRulesFilterToKQL } from '../../../../containers/detection_engine/rules/utils'; +import { useBoolState } from '../../../../../common/hooks/use_bool_state'; +import { useAsyncConfirmation } from '../../../../containers/detection_engine/rules/rules_table/use_async_confirmation'; const INITIAL_SORT_FIELD = 'enabled'; @@ -60,7 +61,7 @@ interface RulesTableProps { history: History; formatUrl: FormatUrl; createPrePackagedRules: CreatePreBuiltRules | null; - hasNoPermissions: boolean; + hasPermissions: boolean; loading: boolean; loadingCreatePrePackagedRules: boolean; refetchPrePackagedRulesStatus: () => Promise; @@ -85,7 +86,7 @@ export const RulesTables = React.memo( history, formatUrl, createPrePackagedRules, - hasNoPermissions, + hasPermissions, loading, loadingCreatePrePackagedRules, refetchPrePackagedRulesStatus, @@ -115,14 +116,12 @@ export const RulesTables = React.memo( }>(DEFAULT_RULES_TABLE_REFRESH_SETTING); const rulesTable = useRulesTable({ - tableRef, initialStateOverride: { isRefreshOn: defaultAutoRefreshSetting.on, }, }); const { - exportRuleIds, filterOptions, loadingRuleIds, loadingRulesAction, @@ -133,12 +132,12 @@ export const RulesTables = React.memo( showIdleModal, isRefreshOn, isRefreshing, + isAllSelected, } = rulesTable.state; const { dispatch, updateOptions, - actionStopped, setShowIdleModal, setLastRefreshDate, setAutoRefreshOn, @@ -186,9 +185,24 @@ export const RulesTables = React.memo( actions, ]); + const [ + isDeleteConfirmationVisible, + showDeleteConfirmation, + hideDeleteConfirmation, + ] = useBoolState(); + + const [confirmDeletion, handleDeletionConfirm, handleDeletionCancel] = useAsyncConfirmation({ + onInit: showDeleteConfirmation, + onFinish: hideDeleteConfirmation, + }); + + const selectedItemsCount = isAllSelected ? pagination.total : selectedRuleIds.length; + const hasPagination = pagination.total > pagination.perPage; + const getBatchItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element[] => { return getBatchItems({ + isAllSelected, closePopover, dispatch, dispatchToaster, @@ -199,9 +213,13 @@ export const RulesTables = React.memo( reFetchRules, refetchPrePackagedRulesStatus, rules, + filterQuery: convertRulesFilterToKQL(filterOptions), + confirmDeletion, + selectedItemsCount, }); }, [ + isAllSelected, dispatch, dispatchToaster, hasMlPermissions, @@ -211,6 +229,9 @@ export const RulesTables = React.memo( rules, selectedRuleIds, hasActionsPrivileges, + filterOptions, + confirmDeletion, + selectedItemsCount, ] ); @@ -219,7 +240,7 @@ export const RulesTables = React.memo( pageIndex: pagination.page - 1, pageSize: pagination.perPage, totalItemCount: pagination.total, - pageSizeOptions: [5, 10, 20, 50, 100, 200, 300, 400, 500, 600], + pageSizeOptions: [5, 10, 20, 50, 100], }), [pagination] ); @@ -252,7 +273,7 @@ export const RulesTables = React.memo( formatUrl, history, hasMlPermissions, - hasNoPermissions, + hasPermissions, loadingRuleIds: loadingRulesAction != null && (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') @@ -268,7 +289,7 @@ export const RulesTables = React.memo( formatUrl, refetchPrePackagedRulesStatus, hasActionsPrivileges, - hasNoPermissions, + hasPermissions, hasMlPermissions, history, loadingRuleIds, @@ -299,15 +320,43 @@ export const RulesTables = React.memo( } }, [createPrePackagedRules, reFetchRules, refetchPrePackagedRulesStatus]); + const isSelectAllCalled = useRef(false); + + // Synchronize selectedRuleIds with EuiBasicTable's selected rows + useValueChanged((ruleIds) => { + if (tableRef.current?.changeSelection != null) { + tableRef.current.setSelection(rules.filter((rule) => ruleIds.includes(rule.id))); + } + }, selectedRuleIds); + const euiBasicTableSelectionProps = useMemo( () => ({ selectable: (item: Rule) => !loadingRuleIds.includes(item.id), - onSelectionChange: (selected: Rule[]) => - dispatch({ type: 'selectedRuleIds', ids: selected.map((r) => r.id) }), + onSelectionChange: (selected: Rule[]) => { + /** + * EuiBasicTable doesn't provide declarative API to control selected rows. + * This limitation requires us to synchronize selection state manually using setSelection(). + * But it creates a chain reaction when the user clicks Select All: + * selectAll() -> setSelection() -> onSelectionChange() -> setSelection(). + * To break the chain we should check whether the onSelectionChange was triggered + * by the Select All action or not. + * + */ + if (isSelectAllCalled.current) { + isSelectAllCalled.current = false; + } else { + dispatch({ type: 'selectedRuleIds', ids: selected.map(({ id }) => id) }); + } + }, }), [loadingRuleIds, dispatch] ); + const toggleSelectAll = useCallback(() => { + isSelectAllCalled.current = true; + dispatch({ type: 'setIsAllSelected', isAllSelected: !isAllSelected }); + }, [dispatch, isAllSelected]); + const refreshTable = useCallback( async (mode: 'auto' | 'manual' = 'manual'): Promise => { if (isLoadingAnActionOnRule) { @@ -397,22 +446,6 @@ export const RulesTables = React.memo( [initLoading, prePackagedRuleStatus, rulesCustomInstalled] ); - const handleGenericDownloaderSuccess = useCallback( - (exportCount) => { - actionStopped(); - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), - color: 'success', - iconType: 'check', - }, - }); - }, - [actionStopped, dispatchToaster] - ); - return ( <> @@ -421,13 +454,6 @@ export const RulesTables = React.memo( - - ( )} {initLoading && ( @@ -492,22 +518,39 @@ export const RulesTables = React.memo(

{i18n.REFRESH_PROMPT_BODY}

)} + {isDeleteConfirmationVisible && ( + +

{i18n.DELETE_CONFIRMATION_BODY}

+
+ )} {shouldShowRulesTable && ( <> { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( void; + numberSelectedItems: number; onGetBatchItemsPopoverContent?: (closePopover: () => void) => JSX.Element[]; + onRefresh?: (refreshRule: boolean) => void; onRefreshSwitch?: (checked: boolean) => void; + onToggleSelectAll?: () => void; + paginationTotal: number; + showBulkActions: boolean; + hasPagination?: boolean; } export const AllRulesUtilityBar = React.memo( ({ - userHasNoPermissions, - onRefresh, - paginationTotal, + canBulkEdit, + isAllSelected, + isAutoRefreshOn, numberSelectedItems, onGetBatchItemsPopoverContent, - isAutoRefreshOn, - showBulkActions = true, + onRefresh, onRefreshSwitch, + onToggleSelectAll, + paginationTotal, + showBulkActions = true, + hasPagination, }) => { const handleGetBatchItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element | null => { @@ -99,7 +105,19 @@ export const AllRulesUtilityBar = React.memo( {i18n.SELECTED_RULES(numberSelectedItems)} - {!userHasNoPermissions && ( + + {canBulkEdit && onToggleSelectAll && hasPagination && ( + + {isAllSelected ? i18n.CLEAR_SELECTION : i18n.SELECT_ALL_RULES(paginationTotal)} + + )} + + {canBulkEdit && ( { ) { history.replace(getDetectionEngineUrl()); return null; - } else if (userHasNoPermissions(canUserCRUD)) { + } else if (!userHasPermissions(canUserCRUD)) { history.replace(getRulesUrl()); return null; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 8dac9e03514d1c..6727db8aba3b40 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -70,7 +70,7 @@ import { } from '../../../../components/alerts_table/default_config'; import { RuleSwitch } from '../../../../components/rules/rule_switch'; import { StepPanel } from '../../../../components/rules/step_panel'; -import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers'; +import { getStepsData, redirectToDetections, userHasPermissions } from '../helpers'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; import { inputsSelectors } from '../../../../../common/store/inputs'; @@ -461,7 +461,7 @@ const RuleDetailsPageComponent = () => { {ruleI18n.EDIT_RULE_SETTINGS} @@ -608,7 +608,7 @@ const RuleDetailsPageComponent = () => { isDisabled={ !isExistingRule || !canEditRuleWithActions(rule, hasActionsPrivileges) || - userHasNoPermissions(canUserCRUD) || + !userHasPermissions(canUserCRUD) || (!hasMlPermissions && !rule?.enabled) } enabled={isExistingRule && (rule?.enabled ?? false)} @@ -625,9 +625,7 @@ const RuleDetailsPageComponent = () => { { ) { history.replace(getDetectionEngineUrl()); return null; - } else if (userHasNoPermissions(canUserCRUD)) { + } else if (!userHasPermissions(canUserCRUD)) { history.replace(getRuleDetailsUrl(ruleId ?? '')); return null; } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 4c3e5b18d4c1b9..fa600d9ce4a0e8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -18,7 +18,7 @@ import { getPrePackagedRuleStatus, getPrePackagedTimelineStatus, determineDetailsValue, - userHasNoPermissions, + userHasPermissions, fillEmptySeverityMappings, } from './helpers'; import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; @@ -403,26 +403,26 @@ describe('rule helpers', () => { }); }); - describe('userHasNoPermissions', () => { - test("returns false when user's CRUD operations are null", () => { - const result: boolean = userHasNoPermissions(null); - const userHasNoPermissionsExpectedResult = false; + describe('userHasPermissions', () => { + test("returns true when user's CRUD operations are null", () => { + const result: boolean = userHasPermissions(null); + const userHasPermissionsExpectedResult = true; - expect(result).toEqual(userHasNoPermissionsExpectedResult); + expect(result).toEqual(userHasPermissionsExpectedResult); }); - test('returns true when user cannot CRUD', () => { - const result: boolean = userHasNoPermissions(false); - const userHasNoPermissionsExpectedResult = true; + test('returns false when user cannot CRUD', () => { + const result: boolean = userHasPermissions(false); + const userHasPermissionsExpectedResult = false; - expect(result).toEqual(userHasNoPermissionsExpectedResult); + expect(result).toEqual(userHasPermissionsExpectedResult); }); - test('returns false when user can CRUD', () => { - const result: boolean = userHasNoPermissions(true); - const userHasNoPermissionsExpectedResult = false; + test('returns true when user can CRUD', () => { + const result: boolean = userHasPermissions(true); + const userHasPermissionsExpectedResult = true; - expect(result).toEqual(userHasNoPermissionsExpectedResult); + expect(result).toEqual(userHasPermissionsExpectedResult); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index a88ff9bb2c9210..f20ace09ed2b61 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -405,8 +405,8 @@ export const getActionMessageParams = memoizeOne( ); // typed as null not undefined as the initial state for this value is null. -export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => - canUserCRUD != null ? !canUserCRUD : false; +export const userHasPermissions = (canUserCRUD: boolean | null): boolean => + canUserCRUD != null ? canUserCRUD : true; export const MaxWidthEuiFlexItem = styled(EuiFlexItem)` max-width: 1000px; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 8aca1cb960c1d2..8bacb10444a7d0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -28,7 +28,7 @@ import { getPrePackagedRuleStatus, getPrePackagedTimelineStatus, redirectToDetections, - userHasNoPermissions, + userHasPermissions, } from './helpers'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../app/types'; @@ -131,7 +131,7 @@ const RulesPageComponent: React.FC = () => { const loadPrebuiltRulesAndTemplatesButton = useMemo( () => getLoadPrebuiltRulesAndTemplatesButton({ - isDisabled: userHasNoPermissions(canUserCRUD) || loading, + isDisabled: !userHasPermissions(canUserCRUD) || loading, onClick: handleCreatePrePackagedRules, }), [canUserCRUD, getLoadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] @@ -140,7 +140,7 @@ const RulesPageComponent: React.FC = () => { const reloadPrebuiltRulesAndTemplatesButton = useMemo( () => getReloadPrebuiltRulesAndTemplatesButton({ - isDisabled: userHasNoPermissions(canUserCRUD) || loading, + isDisabled: !userHasPermissions(canUserCRUD) || loading, onClick: handleCreatePrePackagedRules, }), [canUserCRUD, getReloadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] @@ -213,7 +213,7 @@ const RulesPageComponent: React.FC = () => { { setShowImportModal(true); }} @@ -228,7 +228,7 @@ const RulesPageComponent: React.FC = () => { onClick={goToNewRule} href={formatUrl(getCreateRuleUrl())} iconType="plusInCircle" - isDisabled={userHasNoPermissions(canUserCRUD) || loading} + isDisabled={!userHasPermissions(canUserCRUD) || loading} > {i18n.ADD_NEW_RULE} @@ -250,7 +250,7 @@ const RulesPageComponent: React.FC = () => { data-test-subj="all-rules" loading={loading || prePackagedRuleLoading} loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} - hasNoPermissions={userHasNoPermissions(canUserCRUD)} + hasPermissions={userHasPermissions(canUserCRUD)} refetchPrePackagedRulesStatus={handleRefetchPrePackagedRulesStatus} rulesCustomInstalled={rulesCustomInstalled} rulesInstalled={rulesInstalled} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 1bfa62e9b77c05..defd976a04c4b6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -165,13 +165,13 @@ export const EXPORT_FILENAME = i18n.translate( } ); -export const SUCCESSFULLY_EXPORTED_RULES = (totalRules: number) => +export const SUCCESSFULLY_EXPORTED_RULES = (exportedRules: number, totalRules: number) => i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedRulesTitle', + 'xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedXofYRulesTitle', { - values: { totalRules }, + values: { totalRules, exportedRules }, defaultMessage: - 'Successfully exported {totalRules, plural, =0 {all rules} =1 {{totalRules} rule} other {{totalRules} rules}}', + 'Successfully exported {exportedRules} of {totalRules} {totalRules, plural, =1 {rule} other {rules}}. Prebuilt rules were excluded from the resulting file.', } ); @@ -202,6 +202,19 @@ export const SHOWING_RULES = (totalRules: number) => defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {rule} other {rules}}', }); +export const SELECT_ALL_RULES = (totalRules: number) => + i18n.translate('xpack.securitySolution.detectionEngine.rules.allRules.selectAllRulesTitle', { + values: { totalRules }, + defaultMessage: 'Select all {totalRules} {totalRules, plural, =1 {rule} other {rules}}', + }); + +export const CLEAR_SELECTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.clearSelectionTitle', + { + defaultMessage: 'Clear selection', + } +); + export const SELECTED_RULES = (selectedRules: number) => i18n.translate('xpack.securitySolution.detectionEngine.rules.allRules.selectedRulesTitle', { values: { selectedRules }, @@ -253,6 +266,13 @@ export const DUPLICATE_RULE_ERROR = i18n.translate( } ); +export const BULK_ACTION_FAILED = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.actions.bulkActionFailedDescription', + { + defaultMessage: 'Failed to execte bulk action', + } +); + export const EXPORT_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription', { @@ -577,6 +597,35 @@ export const REFRESH_PROMPT_BODY = i18n.translate( } ); +export const DELETE_CONFIRMATION_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.deleteConfirmationTitle', + { + defaultMessage: 'Confirm bulk deletion', + } +); + +export const DELETE_CONFIRMATION_CONFIRM = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.deleteConfirmationConfirm', + { + defaultMessage: 'Confirm', + } +); + +export const DELETE_CONFIRMATION_CANCEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.deleteConfirmationCancel', + { + defaultMessage: 'Cancel', + } +); + +export const DELETE_CONFIRMATION_BODY = i18n.translate( + 'xpack.securitySolution.detectionEngine.components.allRules.deleteConfirmationBody', + { + defaultMessage: + 'This action will delete all rules that match current filter query. Click "Confirm" to continue.', + } +); + export const REFRESH_RULE_POPOVER_DESCRIPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.refreshRulePopoverDescription', { diff --git a/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/back_to_external_app_button.tsx b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/back_to_external_app_button.tsx new file mode 100644 index 00000000000000..78c854d9335840 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/back_to_external_app_button.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { memo } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import styled from 'styled-components'; + +import { ListPageRouteState } from '../../../../common/endpoint/types'; + +import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; + +const EuiButtonEmptyStyled = styled(EuiButtonEmpty)` + margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; + + .euiIcon { + width: ${({ theme }) => theme.eui.euiIconSizes.small}; + height: ${({ theme }) => theme.eui.euiIconSizes.small}; + } + + .text { + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + } +`; + +export const BackToExternalAppButton = memo( + ({ backButtonLabel, backButtonUrl, onBackButtonNavigateTo }) => { + const handleBackOnClick = useNavigateToAppEventHandler(...onBackButtonNavigateTo!); + + return ( + + {backButtonLabel || ( + + )} + + ); + } +); + +BackToExternalAppButton.displayName = 'BackToExternalAppButton'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/index.ts similarity index 75% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts rename to x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/index.ts index 644ada96a9f051..d4a2f8de135466 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { Phases } from '../../../common/types'; - -export type Phase = keyof Phases; +export { BackToExternalAppButton } from './back_to_external_app_button'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index b62663bd787503..546116f82696b7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { HttpStart } from 'kibana/public'; +import { Dispatch } from 'redux'; +import { CoreStart, HttpStart } from 'kibana/public'; import { EndpointAction, HostInfo, @@ -13,6 +14,7 @@ import { HostIsolationResponse, HostResultList, Immutable, + ImmutableObject, } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareAPI, ImmutableMiddlewareFactory } from '../../../../common/store'; @@ -54,6 +56,7 @@ import { import { isolateHost, unIsolateHost } from '../../../../common/lib/host_isolation'; import { AppAction } from '../../../../common/store/actions'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; +import { ServerReturnedEndpointPackageInfo } from './action'; type EndpointPageStore = ImmutableMiddlewareAPI; @@ -78,26 +81,14 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory, + dispatch: Dispatch, + coreStart: CoreStart +) { + if (endpointPackageInfo(state)) return; + + try { + const packageInfo = await sendGetEndpointSecurityPackage(coreStart.http); + dispatch({ + type: 'serverReturnedEndpointPackageInfo', + payload: packageInfo, + }); + } catch (error) { + // Ignore Errors, since this should not hinder the user's ability to use the UI + // eslint-disable-next-line no-console + console.error(error); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 06b42145a6d48c..4d1ab0f3de8252 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -157,9 +157,9 @@ export const EndpointList = () => { const handleCreatePolicyClick = useNavigateToAppEventHandler( 'fleet', { - path: `#/policies/add-integration${ + path: `#/integrations/${ endpointPackageVersion ? `/endpoint-${endpointPackageVersion}` : '' - }`, + }/add-integration`, state: { onCancelNavigateTo: [ 'securitySolution:administration', diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index 048bd97664f2ea..4171441e7de254 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -111,6 +111,23 @@ describe('Event filter form', () => { expect(store.getState()!.management!.eventFilters!.form!.hasNameError).toBeFalsy(); }); + it('should change name with a white space still shows an error', async () => { + component = renderComponentWithdata(); + + const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER); + + act(() => { + fireEvent.change(nameInput, { + target: { + value: ' ', + }, + }); + }); + + expect(store.getState()!.management!.eventFilters!.form!.entry!.name).toBe(''); + expect(store.getState()!.management!.eventFilters!.form!.hasNameError).toBeTruthy(); + }); + it('should change comments', async () => { component = renderComponentWithdata(); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 93658d4efa737b..121808b62f570d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -91,11 +91,12 @@ export const EventFiltersForm: React.FC = memo( const handleOnChangeName = useCallback( (e: React.ChangeEvent) => { if (!exception) return; + const name = e.target.value.toString().trim(); dispatch({ type: 'eventFiltersChangeForm', payload: { - entry: { ...exception, name: e.target.value.toString() }, - hasNameError: !e.target.value, + entry: { ...exception, name }, + hasNameError: !name, }, }); }, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 465f92dfda767f..59d409874c5615 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -175,4 +175,30 @@ describe('When on the Event Filters List Page', () => { }); }); }); + + describe('and the back button is present', () => { + beforeEach(async () => { + renderResult = render(); + act(() => { + history.push('/event_filters', { + onBackButtonNavigateTo: [{ appId: 'appId' }], + backButtonLabel: 'back to fleet', + backButtonUrl: '/fleet', + }); + }); + }); + + it('back button is present', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + + it('back button is not present', () => { + act(() => { + history.push('/event_filters'); + }); + expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 32fc0182104180..00ee80c5d70223 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { memo, useCallback, useEffect } from 'react'; +import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiSpacer, EuiHorizontalRule, EuiText } from '@elastic/eui'; @@ -34,7 +34,7 @@ import { showDeleteModal, } from '../store/selector'; import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content'; -import { Immutable } from '../../../../../common/endpoint/types'; +import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types'; import { ExceptionItem, ExceptionItemProps, @@ -42,6 +42,7 @@ import { import { EventFilterDeleteModal } from './components/event_filter_delete_modal'; import { SearchBar } from '../../../components/search_bar'; +import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; type EventListPaginatedContent = PaginatedContentProps< Immutable, @@ -59,6 +60,7 @@ const AdministrationListPage = styled(_AdministrationListPage)` `; export const EventFiltersListPage = memo(() => { + const { state: routeState } = useLocation(); const history = useHistory(); const dispatch = useDispatch>(); const isActionError = useEventFiltersSelector(getActionError); @@ -103,6 +105,13 @@ export const EventFiltersListPage = memo(() => { } }, [dispatch, formEntry, history, isActionError, location, navigateCallback]); + const backButton = useMemo(() => { + if (routeState && routeState.onBackButtonNavigateTo) { + return ; + } + return null; + }, [routeState]); + const handleAddButtonClick = useCallback( () => navigateCallback({ @@ -173,6 +182,7 @@ export const EventFiltersListPage = memo(() => { return ( ( }; }, [eventFiltersApi, toasts]); - const eventFiltersRouteState = useMemo(() => { + const eventFiltersRouteState = useMemo(() => { const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; return { backButtonLabel: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index b1464d23e00fbd..ed3ba10c1e62bb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -17,7 +17,7 @@ import { import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { getTrustedAppsListPath } from '../../../../../../common/routing'; import { - TrustedAppsListPageRouteState, + ListPageRouteState, GetExceptionSummaryResponse, } from '../../../../../../../../common/endpoint/types'; import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common'; @@ -67,7 +67,7 @@ export const FleetTrustedAppsCard = memo(( }, [toasts, trustedAppsApi]); const trustedAppsListUrlPath = getTrustedAppsListPath(); - const trustedAppRouteState = useMemo(() => { + const trustedAppRouteState = useMemo(() => { const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; return { backButtonLabel: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 1407e5f64f156a..d01ccea5ba1f4a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -63,7 +63,7 @@ describe('Policy Details', () => { describe('when displayed with invalid id', () => { let releaseApiFailure: () => void; beforeEach(() => { - http.get.mockImplementationOnce(async () => { + http.get.mockImplementation(async () => { await new Promise((_, reject) => { releaseApiFailure = reject.bind(null, new Error('policy not found')); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index fac9fb1e5bf6e5..adc9438f27d743 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -633,18 +633,21 @@ describe('When on the Trusted Apps Page', () => { }); }); - it('should close the flyout', async () => { + it('should close the flyout', () => { expect(renderResult.queryByTestId('addTrustedAppFlyout')).toBeNull(); }); - it('should show success toast notification', async () => { + it('should show success toast notification', () => { expect(coreStart.notifications.toasts.addSuccess.mock.calls[0][0]).toEqual( '"one app" has been added to the Trusted Applications list.' ); }); - it('should trigger the List to reload', async () => { - expect(coreStart.http.get.mock.calls[0][0]).toEqual(TRUSTED_APPS_LIST_API); + it('should trigger the List to reload', () => { + const isCalled = coreStart.http.get.mock.calls.some( + (call) => call[0].toString() === TRUSTED_APPS_LIST_API + ); + expect(isCalled).toEqual(true); }); }); @@ -666,18 +669,18 @@ describe('When on the Trusted Apps Page', () => { }); }); - it('should continue to show the flyout', async () => { + it('should continue to show the flyout', () => { expect(renderResult.getByTestId('addTrustedAppFlyout')).not.toBeNull(); }); - it('should enable the Cancel Button', async () => { + it('should enable the Cancel Button', () => { expect( (renderResult.getByTestId('addTrustedAppFlyout-cancelButton') as HTMLButtonElement) .disabled ).toBe(false); }); - it('should show the dialog close button', async () => { + it('should show the dialog close button', () => { expect(renderResult.getByTestId('euiFlyoutCloseButton')).not.toBeNull(); }); @@ -688,7 +691,7 @@ describe('When on the Trusted Apps Page', () => { ).toBe(false); }); - it('should show API errors in the form', async () => { + it('should show API errors in the form', () => { expect(renderResult.container.querySelector('.euiForm__errors')).not.toBeNull(); }); }); @@ -900,4 +903,34 @@ describe('When on the Trusted Apps Page', () => { }); }); }); + + describe('and the back button is present', () => { + let renderResult: ReturnType; + beforeEach(async () => { + renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsListResourceStateChanged'); + }); + reactTestingLibrary.act(() => { + history.push('/trusted_apps', { + onBackButtonNavigateTo: [{ appId: 'appId' }], + backButtonLabel: 'back to fleet', + backButtonUrl: '/fleet', + }); + }); + }); + + it('back button is present', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + + it('back button is not present', () => { + reactTestingLibrary.act(() => { + history.push('/trusted_apps'); + }); + expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 5603b8e2d61c90..4cd6ad62f3a35f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -7,11 +7,9 @@ import React, { memo, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, - EuiButtonEmpty, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -35,14 +33,14 @@ import { TrustedAppsGrid } from './components/trusted_apps_grid'; import { TrustedAppsList } from './components/trusted_apps_list'; import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; import { TrustedAppsNotifications } from './trusted_apps_notifications'; -import { TrustedAppsListPageRouteState } from '../../../../../common/endpoint/types'; -import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; import { EmptyState } from './components/empty_state'; import { SearchBar } from '../../../components/search_bar'; +import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; +import { ListPageRouteState } from '../../../../../common/endpoint/types'; export const TrustedAppsPage = memo(() => { - const { state: routeState } = useLocation(); + const { state: routeState } = useLocation(); const location = useTrustedAppsSelector(getCurrentLocation); const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); @@ -161,43 +159,3 @@ export const TrustedAppsPage = memo(() => { }); TrustedAppsPage.displayName = 'TrustedAppsPage'; - -const EuiButtonEmptyStyled = styled(EuiButtonEmpty)` - margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; - - .euiIcon { - width: ${({ theme }) => theme.eui.euiIconSizes.small}; - height: ${({ theme }) => theme.eui.euiIconSizes.small}; - } - - .text { - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - } -`; - -const BackToExternalAppButton = memo( - ({ backButtonLabel, backButtonUrl, onBackButtonNavigateTo }) => { - const handleBackOnClick = useNavigateToAppEventHandler(...onBackButtonNavigateTo!); - - return ( - - {backButtonLabel || ( - - )} - - ); - } -); - -BackToExternalAppButton.displayName = 'BackToExternalAppButton'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index a6b545045a6272..f0198092ec1be8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -9,11 +9,15 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { OverviewEmpty } from '.'; import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; + +const endpointPackageVersion = '0.19.1'; + jest.mock('../../../common/lib/kibana'); jest.mock('../../../management/pages/endpoint_hosts/view/hooks', () => ({ useIngestUrl: jest .fn() .mockReturnValue({ appId: 'ingestAppId', appPath: 'ingestPath', url: 'ingestUrl' }), + useEndpointSelector: jest.fn().mockReturnValue({ endpointPackageVersion }), })); jest.mock('../../../common/hooks/endpoint/ingest_enabled', () => ({ @@ -57,7 +61,7 @@ describe('OverviewEmpty', () => { fill: false, label: 'Add Endpoint Security', onClick: undefined, - url: '/app/home#/tutorial_directory/security', + url: `#/integrations/endpoint-${endpointPackageVersion}/add-integration`, }, }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index 6a77271e987715..028871d7be19d8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import { omit } from 'lodash/fp'; +import { createStructuredSelector } from 'reselect'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLink } from '@elastic/eui'; @@ -14,18 +15,33 @@ import * as i18nCommon from '../../../common/translations'; import { EmptyPage, EmptyPageActionsProps } from '../../../common/components/empty_page'; import { useKibana } from '../../../common/lib/kibana'; import { ADD_DATA_PATH } from '../../../../common/constants'; -import { useIngestUrl } from '../../../management/pages/endpoint_hosts/view/hooks'; +import { + useEndpointSelector, + useIngestUrl, +} from '../../../management/pages/endpoint_hosts/view/hooks'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; +import { CreateStructuredSelector } from '../../../common/store'; +import { endpointPackageVersion as useEndpointPackageVersion } from '../../../management/pages/endpoint_hosts/store/selectors'; const OverviewEmptyComponent: React.FC = () => { const { http, docLinks } = useKibana().services; const basePath = http.basePath.get(); - const { appId: ingestAppId, appPath: ingestPath, url: ingestUrl } = useIngestUrl( - 'integrations?category=security' - ); - const handleOnClick = useNavigateToAppEventHandler(ingestAppId, { path: ingestPath }); + const selector = (createStructuredSelector as CreateStructuredSelector)({ + endpointPackageVersion: useEndpointPackageVersion, + }); + const { endpointPackageVersion } = useEndpointSelector(selector); + const { url: ingestUrl } = useIngestUrl(''); + + const endpointIntegrationUrlPath = endpointPackageVersion + ? `/endpoint-${endpointPackageVersion}/add-integration` + : ''; + const endpointIntegrationUrl = `#/integrations${endpointIntegrationUrlPath}`; + const handleEndpointClick = useNavigateToAppEventHandler('fleet', { + path: endpointIntegrationUrl, + }); const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + const emptyPageActions: EmptyPageActionsProps = useMemo( () => ({ elasticAgent: { @@ -42,13 +58,13 @@ const OverviewEmptyComponent: React.FC = () => { }, endpoint: { label: i18nCommon.EMPTY_ACTION_ENDPOINT, - url: `${basePath}${ADD_DATA_PATH}`, + url: endpointIntegrationUrl, description: i18nCommon.EMPTY_ACTION_ENDPOINT_DESCRIPTION, - onClick: handleOnClick, + onClick: handleEndpointClick, fill: false, }, }), - [basePath, ingestUrl, handleOnClick] + [basePath, ingestUrl, endpointIntegrationUrl, handleEndpointClick] ); const emptyPageIngestDisabledActions = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx index a273ef1df97882..738d166fcb9a4a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx @@ -10,14 +10,20 @@ import React from 'react'; import { TimelineDownloader } from './export_timeline'; import { mockSelectedTimeline } from './mocks'; import * as i18n from '../translations'; +import { downloadBlob } from '../../../../common/utils/download_blob'; import { ReactWrapper, mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { exportSelectedTimeline } from '../../../containers/api'; jest.mock('../../../../common/hooks/use_app_toasts'); +jest.mock('../../../../common/utils/download_blob'); +jest.mock('../../../containers/api', () => ({ + exportSelectedTimeline: jest.fn(), +})); jest.mock('.', () => { return { @@ -37,6 +43,7 @@ jest.mock('react-router-dom', () => { describe('TimelineDownloader', () => { const mockAddSuccess = jest.fn(); (useAppToasts as jest.Mock).mockReturnValue({ addSuccess: mockAddSuccess }); + (exportSelectedTimeline as jest.Mock).mockReturnValue(new Blob()); let wrapper: ReactWrapper; const exportedIds = ['baa20980-6301-11ea-9223-95b6d4dd806c']; @@ -56,14 +63,14 @@ describe('TimelineDownloader', () => { mockAddSuccess.mockClear(); }); - describe('should not render a downloader', () => { - test('Without exportedIds', () => { + describe('ExportTimeline', () => { + it('should not start download without exportedIds', () => { const testProps = { ...defaultTestProps, exportedIds: undefined, }; wrapper = mount(); - expect(wrapper.find('[data-test-subj="export-timeline-downloader"]').exists()).toBeFalsy(); + expect(downloadBlob).toHaveBeenCalledTimes(0); }); test('With isEnableDownloader is false', () => { @@ -72,18 +79,23 @@ describe('TimelineDownloader', () => { isEnableDownloader: false, }; wrapper = mount(); - expect(wrapper.find('[data-test-subj="export-timeline-downloader"]').exists()).toBeFalsy(); + expect(downloadBlob).toHaveBeenCalledTimes(0); }); }); - describe('should render a downloader', () => { - test('With selectedItems and exportedIds is given and isEnableDownloader is true', () => { + describe('should start download', () => { + test('With selectedItems and exportedIds is given and isEnableDownloader is true', async () => { const testProps = { ...defaultTestProps, selectedItems: mockSelectedTimeline, }; wrapper = mount(); - expect(wrapper.find('[data-test-subj="export-timeline-downloader"]').exists()).toBeTruthy(); + + await waitFor(() => { + wrapper.update(); + + expect(downloadBlob).toHaveBeenCalledTimes(1); + }); }); test('With correct toast message on success for exported timelines', async () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx index b8b1c76ffd6d7a..10e6ea9ee085c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx @@ -5,23 +5,20 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { - GenericDownloader, - ExportSelectedData, -} from '../../../../common/components/generic_downloader'; import * as i18n from '../translations'; import { TimelineType } from '../../../../../common/types/timeline'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { exportSelectedTimeline } from '../../../containers/api'; +import { downloadBlob } from '../../../../common/utils/download_blob'; const ExportTimeline: React.FC<{ exportedIds: string[] | undefined; - getExportedData: ExportSelectedData; isEnableDownloader: boolean; onComplete?: () => void; -}> = ({ onComplete, isEnableDownloader, exportedIds, getExportedData }) => { +}> = ({ onComplete, isEnableDownloader, exportedIds }) => { const { tabName: timelineType } = useParams<{ tabName: TimelineType }>(); const { addSuccess } = useAppToasts(); @@ -47,20 +44,28 @@ const ExportTimeline: React.FC<{ } }, [onComplete]); - return ( - <> - {exportedIds != null && isEnableDownloader && ( - - )} - - ); + useEffect(() => { + const downloadTimeline = async () => { + if (exportedIds?.length && isEnableDownloader) { + const result = await exportSelectedTimeline({ ids: exportedIds }); + if (result instanceof Blob) { + downloadBlob(result, `${i18n.EXPORT_FILENAME}.ndjson`); + onExportSuccess(exportedIds.length); + } else { + onExportFailure(); + } + } + }; + + downloadTimeline(); + // We probably don't need to have ExportTimeline in the form of a React component. + // See https://github.com/elastic/kibana/issues/101571 for more detail. + // But for now, it uses isEnableDownloader as a signal to start downloading. + // Other variables are excluded from the deps array to avoid false positives + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [exportedIds, isEnableDownloader]); + + return null; }; ExportTimeline.displayName = 'ExportTimeline'; export const TimelineDownloader = React.memo(ExportTimeline); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx index 250e7847edb5c8..aa447c9e84f97a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx @@ -10,7 +10,6 @@ import { DeleteTimelines } from '../types'; import { TimelineDownloader } from './export_timeline'; import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; -import { exportSelectedTimeline } from '../../../containers/api'; export interface ExportTimeline { disableExportTimelineDownloader: () => void; @@ -37,7 +36,6 @@ export const EditTimelineActionsComponent: React.FC<{ @@ -55,4 +53,3 @@ export const EditTimelineActionsComponent: React.FC<{ ); export const EditTimelineActions = React.memo(EditTimelineActionsComponent); -export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); 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 316b6cff766eab..922e40d6d860e4 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 @@ -24,7 +24,7 @@ import { importTimelines } from '../../containers/api'; import { useEditTimelineBatchActions } from './edit_timeline_batch_actions'; import { useEditTimelineActions } from './edit_timeline_actions'; -import { EditOneTimelineAction } from './export_timeline'; +import { EditTimelineActions } from './export_timeline'; import { SearchRow } from './search_row'; import { TimelinesTable } from './timelines_table'; import * as i18n from './translations'; @@ -170,7 +170,7 @@ export const OpenTimeline = React.memo( return ( <> - => { +}: ExportDocumentsProps): Promise => { let requestBody; try { requestBody = ids.length > 0 ? JSON.stringify({ ids }) : undefined; 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 857762dec45e94..3942d1637fedd1 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 @@ -18,6 +18,7 @@ import { DETECTION_ENGINE_PREPACKAGED_URL, DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL, DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL, + DETECTION_ENGINE_RULES_BULK_ACTION, } from '../../../../../common/constants'; import { ShardsResponse } from '../../../types'; import { @@ -36,6 +37,7 @@ import { getSignalsMigrationStatusSchemaMock } from '../../../../../common/detec import { RuleParams } from '../../schemas/rule_schemas'; import { Alert } from '../../../../../../alerting/common'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; +import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -107,6 +109,13 @@ export const getPatchBulkRequest = () => body: [getCreateRulesSchemaMock()], }); +export const getBulkActionRequest = () => + requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_BULK_ACTION, + body: getPerformBulkActionSchemaMock(), + }); + export const getDeleteBulkRequest = () => requestMock.create({ method: 'delete', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 1abe55b782c328..4f060746b92b0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -7,7 +7,7 @@ Object { ], "mappings": Object { "_meta": Object { - "version": 35, + "version": 45, }, "dynamic": false, "properties": Object { @@ -365,6 +365,19 @@ Object { }, }, }, + "data_stream": Object { + "properties": Object { + "dataset": Object { + "type": "keyword", + }, + "namespace": Object { + "type": "keyword", + }, + "type": Object { + "type": "keyword", + }, + }, + }, "destination": Object { "properties": Object { "address": Object { @@ -1907,6 +1920,54 @@ Object { }, }, }, + "orchestrator": Object { + "properties": Object { + "api_version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "cluster": Object { + "properties": Object { + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "url": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "version": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "namespace": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "organization": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "resource": Object { + "properties": Object { + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "type": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, "organization": Object { "properties": Object { "id": Object { @@ -4467,6 +4528,6 @@ Object { }, }, }, - "version": 35, + "version": 45, } `; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json index 2967f4cb725e74..3d24384680f575 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/ecs_mapping.json @@ -1,10 +1,8 @@ { - "index_patterns": [ - "try-ecs-*" - ], + "index_patterns": ["try-ecs-*"], "mappings": { "_meta": { - "version": "1.9.0" + "version": "1.10.0" }, "date_detection": false, "dynamic_templates": [ @@ -331,6 +329,19 @@ } } }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, "destination": { "properties": { "address": { @@ -1802,6 +1813,54 @@ } } }, + "orchestrator": { + "properties": { + "api_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "organization": { "properties": { "id": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 9c39ad4ee35982..4691db1b19595e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -43,6 +43,49 @@ describe('get_signals_template', () => { expect(template.settings.mapping.total_fields.limit).toBeGreaterThanOrEqual(10000); }); + // If you see this test fail, you should track down any and all "constant_keyword" in your ecs_mapping.json and replace + // those with "keyword". The paths that fail in the array below will be something like: + // - Expected - 1 + // + Received + 5 + // + // - Array [] + // + Array [ + // + "mappings.properties.data_stream.properties.dataset", + // + "mappings.properties.data_stream.properties.namespace", + // + "mappings.properties.data_stream.properties.type", + // + ] + // which means that in your ecs_mapping you have paths such as "mappings.properties.data_stream.properties.dataset" which + // contain a constant_keyword which needs to be replaced with a normal keyword instead. + // + // The reason why we deviate from ECS standards here is because when you have a many to 1 relationship where you have + // several different indexes with different "constant_keyword" values you cannot copy them over into a single "constant_keyword". + // Instead you have to use "keyword". This test was first introduced when ECS 1.10 came out and data_stream.* values which had + // "constant_keyword" fields and we needed to change those to be "keyword" instead. + test('it should NOT have any "constant_keyword" and instead those should be replaced with regular "keyword" in the mapping', () => { + const template = getSignalsTemplate('test-index'); + + // Small recursive function to find any values of "constant_keyword" and mark which fields it was found on and then error on those fields + // The matchers from jest such as jest.toMatchObject do not support recursion, so I have to write it here: + // https://github.com/facebook/jest/issues/2506 + const recursiveConstantKeywordFound = (path: string, inputTemplate: object): string[] => + Object.entries(inputTemplate).reduce((accum, [key, innerValue]) => { + if (typeof innerValue === 'object') { + return [ + ...accum, + ...recursiveConstantKeywordFound(path !== '' ? `${path}.${key}` : key, innerValue), + ]; + } else { + if (key === 'type' && innerValue === 'constant_keyword') { + return [...accum, path]; + } else { + return accum; + } + } + }, []); + const constantKeywordsFound = recursiveConstantKeywordFound('', template); + expect(constantKeywordsFound).toEqual([]); + }); + test('it should match snapshot', () => { const template = getSignalsTemplate('test-index'); expect(template).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index 0318218ed59001..53035ebf28cd78 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -22,7 +22,7 @@ import otherMapping from './other_mappings.json'; incremented by 10 in order to add "room" for the aforementioned patch release */ -export const SIGNALS_TEMPLATE_VERSION = 35; +export const SIGNALS_TEMPLATE_VERSION = 45; export const MIN_EQL_RULE_INDEX_VERSION = 2; export const getSignalsTemplate = (index: string) => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 1e7ba976d6915f..3068521682f8fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -23,9 +23,8 @@ import { getIdBulkError } from './utils'; import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { deleteRules } from '../../rules/delete_rules'; -import { deleteNotifications } from '../../notifications/delete_notifications'; -import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { readRules } from '../../rules/read_rules'; type Config = RouteConfig; type Handler = RequestHandler< @@ -74,27 +73,24 @@ export const deleteRulesBulkRoute = (router: SecuritySolutionPluginRouter) => { } try { - const rule = await deleteRules({ - alertsClient, - id, - ruleId, - }); - if (rule != null) { - await deleteNotifications({ alertsClient, ruleAlertId: rule.id }); - await deleteRuleActionsSavedObject({ - ruleAlertId: rule.id, - savedObjectsClient, - }); - const ruleStatuses = await ruleStatusClient.find({ - perPage: 6, - search: rule.id, - searchFields: ['alertId'], - }); - ruleStatuses.saved_objects.forEach(async (obj) => ruleStatusClient.delete(obj.id)); - return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses); - } else { + const rule = await readRules({ alertsClient, id, ruleId }); + if (!rule) { return getIdBulkError({ id, ruleId }); } + + const ruleStatuses = await ruleStatusClient.find({ + perPage: 6, + search: rule.id, + searchFields: ['alertId'], + }); + await deleteRules({ + alertsClient, + savedObjectsClient, + ruleStatusClient, + ruleStatuses, + id: rule.id, + }); + return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses); } catch (err) { return transformBulkError(idOrRuleIdOrUnknown, err); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 4b05f603b85b7c..4a6b41230f799e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -19,9 +19,8 @@ import { deleteRules } from '../../rules/delete_rules'; import { getIdError, transform } from './utils'; import { buildSiemResponse } from '../utils'; -import { deleteNotifications } from '../../notifications/delete_notifications'; -import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { readRules } from '../../rules/read_rules'; export const deleteRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -57,36 +56,33 @@ export const deleteRulesRoute = ( } const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - const rule = await deleteRules({ - alertsClient, - id, - ruleId, - }); - if (rule != null) { - await deleteNotifications({ alertsClient, ruleAlertId: rule.id }); - await deleteRuleActionsSavedObject({ - ruleAlertId: rule.id, - savedObjectsClient, - }); - const ruleStatuses = await ruleStatusClient.find({ - perPage: 6, - search: rule.id, - searchFields: ['alertId'], - }); - ruleStatuses.saved_objects.forEach(async (obj) => ruleStatusClient.delete(obj.id)); - const transformed = transform(rule, undefined, ruleStatuses.saved_objects[0]); - if (transformed == null) { - return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' }); - } else { - return response.ok({ body: transformed ?? {} }); - } - } else { + const rule = await readRules({ alertsClient, id, ruleId }); + if (!rule) { const error = getIdError({ id, ruleId }); return siemResponse.error({ body: error.message, statusCode: error.statusCode, }); } + + const ruleStatuses = await ruleStatusClient.find({ + perPage: 6, + search: rule.id, + searchFields: ['alertId'], + }); + await deleteRules({ + alertsClient, + savedObjectsClient, + ruleStatusClient, + ruleStatuses, + id: rule.id, + }); + const transformed = transform(rule, undefined, ruleStatuses.saved_objects[0]); + if (transformed == null) { + return siemResponse.error({ statusCode: 500, body: 'failed to transform alert' }); + } else { + return response.ok({ body: transformed ?? {} }); + } } catch (err) { const error = transformError(err); return siemResponse.error({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts new file mode 100644 index 00000000000000..60677fd8eda90c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { + getEmptyFindResult, + getFindResultStatus, + getBulkActionRequest, + getFindResultWithSingleHit, + getFindResultWithMultiHits, +} from '../__mocks__/request_responses'; +import { requestContextMock, serverMock, requestMock } from '../__mocks__'; +import { performBulkActionRoute } from './perform_bulk_action_route'; +import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; + +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); + +describe('perform_bulk_action', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; + + beforeEach(() => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.createSetupContract(); + + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + + performBulkActionRoute(server.router, ml); + }); + + describe('status codes', () => { + it('returns 200 when performing bulk action with all dependencies present', async () => { + const response = await server.inject(getBulkActionRequest(), context); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ success: true, rules_count: 1 }); + }); + + it("returns 200 when provided filter query doesn't match any rules", async () => { + clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); + const response = await server.inject(getBulkActionRequest(), context); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ success: true, rules_count: 0 }); + }); + + it('returns 400 when provided filter query matches too many rules', async () => { + clients.alertsClient.find.mockResolvedValue( + getFindResultWithMultiHits({ data: [], total: Infinity }) + ); + const response = await server.inject(getBulkActionRequest(), context); + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: 'More than 10000 rules matched the filter query. Try to narrow it down.', + status_code: 400, + }); + }); + + it('returns 404 if alertClient is not available on the route', async () => { + context.alerting!.getAlertsClient = jest.fn(); + const response = await server.inject(getBulkActionRequest(), context); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + + it('catches error if disable throws error', async () => { + clients.alertsClient.disable.mockImplementation(async () => { + throw new Error('Test error'); + }); + const response = await server.inject(getBulkActionRequest(), context); + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Test error', + status_code: 500, + }); + }); + + it('rejects patching a rule if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); + const response = await server.inject(getBulkActionRequest(), context); + + expect(response.status).toEqual(403); + expect(response.body).toEqual({ + message: 'mocked validation message', + status_code: 403, + }); + }); + }); + + describe('request validation', () => { + it('rejects payloads with no action', async () => { + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_BULK_ACTION, + body: { ...getPerformBulkActionSchemaMock(), action: undefined }, + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith( + 'Invalid value "undefined" supplied to "action"' + ); + }); + + it('rejects payloads with unknown action', async () => { + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_BULK_ACTION, + body: { ...getPerformBulkActionSchemaMock(), action: 'unknown' }, + }); + const result = server.validate(request); + expect(result.badRequest).toHaveBeenCalledWith( + 'Invalid value "unknown" supplied to "action"' + ); + }); + + it('accepts payloads with no query', async () => { + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_BULK_ACTION, + body: { ...getPerformBulkActionSchemaMock(), query: undefined }, + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + + it('accepts payloads with query and action', async () => { + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_BULK_ACTION, + body: getPerformBulkActionSchemaMock(), + }); + const result = server.validate(request); + + expect(result.ok).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts new file mode 100644 index 00000000000000..9d569acf3782ae --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -0,0 +1,172 @@ +/* + * 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 { transformError } from '@kbn/securitysolution-es-utils'; +import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; +import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { performBulkActionSchema } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; +import { SetupPlugins } from '../../../../plugin'; +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; +import { deleteRules } from '../../rules/delete_rules'; +import { duplicateRule } from '../../rules/duplicate_rule'; +import { enableRule } from '../../rules/enable_rule'; +import { findRules } from '../../rules/find_rules'; +import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { buildSiemResponse } from '../utils'; + +const BULK_ACTION_RULES_LIMIT = 10000; + +export const performBulkActionRoute = ( + router: SecuritySolutionPluginRouter, + ml: SetupPlugins['ml'] +) => { + router.post( + { + path: DETECTION_ENGINE_RULES_BULK_ACTION, + validate: { + body: buildRouteValidation(performBulkActionSchema), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const { body } = request; + const siemResponse = buildSiemResponse(response); + + try { + const alertsClient = context.alerting?.getAlertsClient(); + const savedObjectsClient = context.core.savedObjects.client; + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + + const mlAuthz = buildMlAuthz({ + license: context.licensing.license, + ml, + request, + savedObjectsClient, + }); + + if (!alertsClient) { + return siemResponse.error({ statusCode: 404 }); + } + + const rules = await findRules({ + alertsClient, + perPage: BULK_ACTION_RULES_LIMIT, + filter: body.query !== '' ? body.query : undefined, + page: undefined, + sortField: undefined, + sortOrder: undefined, + fields: undefined, + }); + + if (rules.total > BULK_ACTION_RULES_LIMIT) { + return siemResponse.error({ + body: `More than ${BULK_ACTION_RULES_LIMIT} rules matched the filter query. Try to narrow it down.`, + statusCode: 400, + }); + } + + switch (body.action) { + case BulkAction.enable: + await Promise.all( + rules.data.map(async (rule) => { + if (!rule.enabled) { + throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); + await enableRule({ rule, alertsClient, savedObjectsClient }); + } + }) + ); + break; + case BulkAction.disable: + await Promise.all( + rules.data.map(async (rule) => { + if (rule.enabled) { + throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); + await alertsClient.disable({ id: rule.id }); + } + }) + ); + break; + case BulkAction.delete: + await Promise.all( + rules.data.map(async (rule) => { + const ruleStatuses = await ruleStatusClient.find({ + perPage: 6, + search: rule.id, + searchFields: ['alertId'], + }); + await deleteRules({ + alertsClient, + savedObjectsClient, + ruleStatusClient, + ruleStatuses, + id: rule.id, + }); + }) + ); + break; + case BulkAction.duplicate: + await Promise.all( + rules.data.map(async (rule) => { + throwHttpError(await mlAuthz.validateRuleType(rule.params.type)); + + const createdRule = await alertsClient.create({ + data: duplicateRule(rule), + }); + + const ruleActions = await getRuleActionsSavedObject({ + savedObjectsClient, + ruleAlertId: rule.id, + }); + + await updateRulesNotifications({ + ruleAlertId: createdRule.id, + alertsClient, + savedObjectsClient, + enabled: createdRule.enabled, + actions: ruleActions?.actions || [], + throttle: ruleActions?.alertThrottle, + name: createdRule.name, + }); + }) + ); + break; + case BulkAction.export: + const exported = await getExportByObjectIds( + alertsClient, + rules.data.map(({ params }) => ({ rule_id: params.ruleId })) + ); + + const responseBody = `${exported.rulesNdjson}${exported.exportDetails}`; + + return response.ok({ + headers: { + 'Content-Disposition': `attachment; filename="rules_export.ndjson"`, + 'Content-Type': 'application/ndjson', + }, + body: responseBody, + }); + } + + return response.ok({ body: { success: true, rules_count: rules.data.length } }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts index 972e44c718f94e..a871c7157d5e83 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.test.ts @@ -33,8 +33,8 @@ describe('add_tags', () => { const tags2 = addTags(tags1, 'rule-1', false); expect(tags2).toEqual([ 'tag-1', - `${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`, + `${INTERNAL_RULE_ID_KEY}:rule-1`, ]); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts index 84a24839384210..6ff4a54ad8e544 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/add_tags.ts @@ -10,7 +10,7 @@ import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common export const addTags = (tags: string[], ruleId: string, immutable: boolean): string[] => { return Array.from( new Set([ - ...tags, + ...tags.filter((tag) => !tag.startsWith(INTERNAL_RULE_ID_KEY)), `${INTERNAL_RULE_ID_KEY}:${ruleId}`, `${INTERNAL_IMMUTABLE_KEY}:${immutable}`, ]) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index e7a4df790d62d2..f581be9e1f62b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -5,138 +5,74 @@ * 2.0. */ +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { alertsClientMock } from '../../../../../alerting/server/mocks'; +import { ruleStatusSavedObjectsClientMock } from '../signals/__mocks__/rule_status_saved_objects_client.mock'; import { deleteRules } from './delete_rules'; -import { readRules } from './read_rules'; -jest.mock('./read_rules'); +import { deleteNotifications } from '../notifications/delete_notifications'; +import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object'; +import { SavedObjectsFindResult } from '../../../../../../../src/core/server'; +import { IRuleStatusSOAttributes } from './types'; + +jest.mock('../notifications/delete_notifications'); +jest.mock('../rule_actions/delete_rule_actions_saved_object'); describe('deleteRules', () => { let alertsClient: ReturnType; - const notificationId = 'notification-52128c15-0d1b-4716-a4c5-46997ac7f3bd'; - const ruleId = 'rule-04128c15-0d1b-4716-a4c5-46997ac7f3bd'; + let ruleStatusClient: ReturnType; + let savedObjectsClient: ReturnType; beforeEach(() => { alertsClient = alertsClientMock.create(); + savedObjectsClient = savedObjectsClientMock.create(); + ruleStatusClient = ruleStatusSavedObjectsClientMock.create(); }); - it('should return null if notification was not found', async () => { - (readRules as jest.Mock).mockResolvedValue(null); - - const result = await deleteRules({ - alertsClient, - id: notificationId, - ruleId, - }); - - expect(result).toBe(null); - }); - - it('should call alertsClient.delete if notification was found', async () => { - (readRules as jest.Mock).mockResolvedValue({ - id: notificationId, - }); - - const result = await deleteRules({ - alertsClient, - id: notificationId, - ruleId, - }); - - expect(alertsClient.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: notificationId, - }) - ); - expect(result).toEqual({ id: notificationId }); - }); - - it('should call alertsClient.delete if ruleId was undefined', async () => { - (readRules as jest.Mock).mockResolvedValue({ - id: null, - }); - - const result = await deleteRules({ - alertsClient, - id: notificationId, - ruleId: undefined, - }); - - expect(alertsClient.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: notificationId, - }) - ); - expect(result).toEqual({ id: null }); - }); - - it('should return null if alertsClient.delete rejects with 404 if ruleId was undefined', async () => { - (readRules as jest.Mock).mockResolvedValue({ - id: null, - }); - - alertsClient.delete.mockRejectedValue({ - output: { - statusCode: 404, + it('should delete the rule along with its notifications, actions, and statuses', async () => { + const ruleStatus: SavedObjectsFindResult = { + id: 'statusId', + type: '', + references: [], + attributes: { + alertId: 'alertId', + statusDate: '', + lastFailureAt: null, + lastFailureMessage: null, + lastSuccessAt: null, + lastSuccessMessage: null, + status: null, + lastLookBackDate: null, + gap: null, + bulkCreateTimeDurations: null, + searchAfterTimeDurations: null, }, - }); + score: 0, + }; - const result = await deleteRules({ + const rule = { alertsClient, - id: notificationId, - ruleId: undefined, - }); - - expect(alertsClient.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: notificationId, - }) - ); - expect(result).toEqual(null); - }); - - it('should return error object if alertsClient.delete rejects with status different than 404 and if ruleId was undefined', async () => { - (readRules as jest.Mock).mockResolvedValue({ - id: null, - }); - - const errorObject = { - output: { - statusCode: 500, + savedObjectsClient, + ruleStatusClient, + id: 'ruleId', + ruleStatuses: { + total: 0, + per_page: 0, + page: 0, + saved_objects: [ruleStatus], }, }; - alertsClient.delete.mockRejectedValue(errorObject); + await deleteRules(rule); - let errorResult; - try { - await deleteRules({ - alertsClient, - id: notificationId, - ruleId: undefined, - }); - } catch (error) { - errorResult = error; - } - - expect(alertsClient.delete).toHaveBeenCalledWith( - expect.objectContaining({ - id: notificationId, - }) - ); - expect(errorResult).toEqual(errorObject); - }); - - it('should return null if ruleId and id was undefined', async () => { - (readRules as jest.Mock).mockResolvedValue({ - id: null, + expect(alertsClient.delete).toHaveBeenCalledWith({ id: rule.id }); + expect(deleteNotifications).toHaveBeenCalledWith({ + ruleAlertId: rule.id, + alertsClient: expect.any(Object), }); - - const result = await deleteRules({ - alertsClient, - id: undefined, - ruleId: undefined, + expect(deleteRuleActionsSavedObject).toHaveBeenCalledWith({ + ruleAlertId: rule.id, + savedObjectsClient: expect.any(Object), }); - - expect(result).toEqual(null); + expect(ruleStatusClient.delete).toHaveBeenCalledWith(ruleStatus.id); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts index 3947103e7625d8..ed5477599253bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.ts @@ -5,30 +5,19 @@ * 2.0. */ -import { readRules } from './read_rules'; +import { deleteNotifications } from '../notifications/delete_notifications'; +import { deleteRuleActionsSavedObject } from '../rule_actions/delete_rule_actions_saved_object'; import { DeleteRuleOptions } from './types'; -export const deleteRules = async ({ alertsClient, id, ruleId }: DeleteRuleOptions) => { - const rule = await readRules({ alertsClient, id, ruleId }); - if (rule == null) { - return null; - } - - if (ruleId != null) { - await alertsClient.delete({ id: rule.id }); - return rule; - } else if (id != null) { - try { - await alertsClient.delete({ id }); - return rule; - } catch (err) { - if (err.output.statusCode === 404) { - return null; - } else { - throw err; - } - } - } else { - return null; - } +export const deleteRules = async ({ + alertsClient, + savedObjectsClient, + ruleStatusClient, + ruleStatuses, + id, +}: DeleteRuleOptions) => { + await alertsClient.delete({ id }); + await deleteNotifications({ alertsClient, ruleAlertId: id }); + await deleteRuleActionsSavedObject({ ruleAlertId: id, savedObjectsClient }); + ruleStatuses.saved_objects.forEach(async (obj) => ruleStatusClient.delete(obj.id)); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts new file mode 100644 index 00000000000000..3046999a632c62 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; +import { duplicateRule } from './duplicate_rule'; + +jest.mock('uuid', () => ({ + v4: jest.fn(), +})); + +describe('duplicateRule', () => { + it('should return a copy of rule with new ruleId', () => { + (uuid.v4 as jest.Mock).mockReturnValue('newId'); + + expect( + duplicateRule({ + id: 'oldTestRuleId', + notifyWhen: 'onActiveAlert', + name: 'test', + tags: ['test', '__internal_rule_id:oldTestRuleId', `${INTERNAL_IMMUTABLE_KEY}:false`], + alertTypeId: 'siem.signals', + consumer: 'siem', + params: { + savedId: undefined, + author: [], + description: 'test', + ruleId: 'oldTestRuleId', + falsePositives: [], + from: 'now-360s', + immutable: false, + license: '', + outputIndex: '.siem-signals-default', + meta: undefined, + maxSignals: 100, + riskScore: 42, + riskScoreMapping: [], + severity: 'low', + severityMapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + type: 'query', + language: 'kuery', + index: [], + query: 'process.args : "chmod"', + filters: [], + buildingBlockType: undefined, + note: undefined, + timelineId: undefined, + timelineTitle: undefined, + ruleNameOverride: undefined, + timestampOverride: undefined, + }, + schedule: { + interval: '5m', + }, + enabled: false, + actions: [], + throttle: null, + apiKeyOwner: 'kibana', + createdBy: 'kibana', + updatedBy: 'kibana', + muteAll: false, + mutedInstanceIds: [], + updatedAt: new Date(2021, 0), + createdAt: new Date(2021, 0), + scheduledTaskId: undefined, + executionStatus: { + lastExecutionDate: new Date(2021, 0), + status: 'ok', + }, + }) + ).toMatchInlineSnapshot(` + Object { + "actions": Array [], + "alertTypeId": "siem.signals", + "consumer": "siem", + "enabled": false, + "name": "test [Duplicate]", + "notifyWhen": null, + "params": Object { + "author": Array [], + "buildingBlockType": undefined, + "description": "test", + "exceptionsList": Array [], + "falsePositives": Array [], + "filters": Array [], + "from": "now-360s", + "immutable": false, + "index": Array [], + "language": "kuery", + "license": "", + "maxSignals": 100, + "meta": undefined, + "note": undefined, + "outputIndex": ".siem-signals-default", + "query": "process.args : \\"chmod\\"", + "references": Array [], + "riskScore": 42, + "riskScoreMapping": Array [], + "ruleId": "newId", + "ruleNameOverride": undefined, + "savedId": undefined, + "severity": "low", + "severityMapping": Array [], + "threat": Array [], + "timelineId": undefined, + "timelineTitle": undefined, + "timestampOverride": undefined, + "to": "now", + "type": "query", + "version": 1, + }, + "schedule": Object { + "interval": "5m", + }, + "tags": Array [ + "test", + "__internal_immutable:false", + "__internal_rule_id:newId", + ], + "throttle": null, + } + `); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts new file mode 100644 index 00000000000000..2f12e335074227 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/duplicate_rule.ts @@ -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 uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { SanitizedAlert } from '../../../../../alerting/common'; +import { SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; +import { InternalRuleCreate, RuleParams } from '../schemas/rule_schemas'; +import { addTags } from './add_tags'; + +const DUPLICATE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', + { + defaultMessage: 'Duplicate', + } +); + +export const duplicateRule = (rule: SanitizedAlert): InternalRuleCreate => { + const newRuleId = uuid.v4(); + return { + name: `${rule.name} [${DUPLICATE_TITLE}]`, + tags: addTags(rule.tags, newRuleId, false), + alertTypeId: SIGNALS_ID, + consumer: SERVER_APP_ID, + params: { + ...rule.params, + immutable: false, + ruleId: newRuleId, + }, + schedule: rule.schedule, + enabled: false, + actions: rule.actions, + throttle: null, + notifyWhen: null, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts new file mode 100644 index 00000000000000..dc4cca2059b3e3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { SanitizedAlert } from '../../../../../alerting/common'; +import { AlertsClient } from '../../../../../alerting/server'; +import { RuleParams } from '../schemas/rule_schemas'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; + +interface EnableRuleArgs { + rule: SanitizedAlert; + alertsClient: AlertsClient; + savedObjectsClient: SavedObjectsClientContract; +} + +/** + * Enables the rule and updates its status to 'going to run' + * + * @param rule - rule to enable + * @param alertsClient - Alerts client + * @param savedObjectsClient - Saved Objects client + */ +export const enableRule = async ({ rule, alertsClient, savedObjectsClient }: EnableRuleArgs) => { + await alertsClient.enable({ id: rule.id }); + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + + // set current status for this rule to be 'going to run' + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + await ruleStatusClient.update(currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + status: 'going to run', + }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts index 754aaf67c32243..eae5ccd2f6ffde 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts @@ -18,7 +18,7 @@ export const getFilter = (filter: string | null | undefined) => { } }; -export const findRules = async ({ +export const findRules = ({ alertsClient, perPage, page, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index b9a88bc36a812f..72af7ebc340cd9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { defaults } from 'lodash/fp'; import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { PatchRulesOptions } from './types'; -import { addTags } from './add_tags'; -import { calculateVersion, calculateName, calculateInterval, removeUndefined } from './utils'; -import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; -import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { normalizeMachineLearningJobIds, normalizeThresholdObject, } from '../../../../common/detection_engine/utils'; +import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; +import { addTags } from './add_tags'; +import { enableRule } from './enable_rule'; +import { PatchRulesOptions } from './types'; +import { calculateInterval, calculateName, calculateVersion, removeUndefined } from './utils'; class PatchError extends Error { public readonly statusCode: number; @@ -200,25 +200,7 @@ export const patchRules = async ({ if (rule.enabled && enabled === false) { await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { - await alertsClient.enable({ id: rule.id }); - - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - const ruleCurrentStatus = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }); - - // set current status for this rule to be 'going to run' - if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { - const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - await ruleStatusClient.update(currentStatusToDisable.id, { - ...currentStatusToDisable.attributes, - status: 'going to run', - }); - } + await enableRule({ rule, alertsClient, savedObjectsClient }); } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 601f3ebaa0f9e6..d029393ce781e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -68,6 +68,7 @@ import { MetaOrUndefined, Description, Enabled, + Id, IdOrUndefined, RuleIdOrUndefined, EnabledOrUndefined, @@ -105,6 +106,7 @@ import { Alert, SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; +import { RuleStatusSavedObjectsClient } from '../signals/rule_status_saved_objects_client'; export type RuleAlertType = Alert; @@ -329,8 +331,10 @@ export interface ReadRuleOptions { export interface DeleteRuleOptions { alertsClient: AlertsClient; - id: IdOrUndefined; - ruleId: RuleIdOrUndefined; + savedObjectsClient: SavedObjectsClientContract; + ruleStatusClient: RuleStatusSavedObjectsClient; + ruleStatuses: SavedObjectsFindResponse; + id: Id; } export interface FindRuleOptions { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index 38cae8d1cf50fa..0fac804163afab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -13,9 +13,9 @@ import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; import { InternalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; +import { enableRule } from './enable_rule'; export const updateRules = async ({ alertsClient, @@ -88,25 +88,7 @@ export const updateRules = async ({ if (existingRule.enabled && enabled === false) { await alertsClient.disable({ id: existingRule.id }); } else if (!existingRule.enabled && enabled === true) { - await alertsClient.enable({ id: existingRule.id }); - - const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); - const ruleCurrentStatus = await ruleStatusClient.find({ - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: existingRule.id, - searchFields: ['alertId'], - }); - - // set current status for this rule to be 'going to run' - if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { - const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - await ruleStatusClient.update(currentStatusToDisable.id, { - ...currentStatusToDisable.attributes, - status: 'going to run', - }); - } + await enableRule({ rule: existingRule, alertsClient, savedObjectsClient }); } return { ...update, enabled }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh new file mode 100755 index 00000000000000..d78726f9e23b5a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example delete all rules +# ./delete_rules_by_query.sh + +# Example delete rules with tag "test" +# ./delete_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"delete\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh new file mode 100755 index 00000000000000..1c7f05f4057cf1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example disable all rules +# ./disable_rules_by_query.sh + +# Example disable rules with tag "test" +# ./disable_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"disable\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh new file mode 100755 index 00000000000000..13da480d74f83d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example duplicate all rules +# ./duplicate_rules_by_query.sh + +# Example duplicate rules with tag "test" +# ./duplicate_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"duplicate\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh new file mode 100755 index 00000000000000..52bb51b6dd6dfb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example enable all rules +# ./enable_rules_by_query.sh + +# Example enable rules with tag "test" +# ./enable_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"enable\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh new file mode 100755 index 00000000000000..954fc2104262b6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example export all rules +# ./export_rules_by_query.sh + +# Example export rules with tag "test" +# ./export_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"export\" +}" diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 0245d4cb99cc0f..00de66c0dec284 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -31,6 +31,7 @@ import { createRulesBulkRoute } from '../lib/detection_engine/routes/rules/creat import { updateRulesBulkRoute } from '../lib/detection_engine/routes/rules/update_rules_bulk_route'; import { patchRulesBulkRoute } from '../lib/detection_engine/routes/rules/patch_rules_bulk_route'; import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delete_rules_bulk_route'; +import { performBulkActionRoute } from '../lib/detection_engine/routes/rules/perform_bulk_action_route'; import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; @@ -81,6 +82,7 @@ export const initRoutes = ( updateRulesBulkRoute(router, ml); patchRulesBulkRoute(router, ml); deleteRulesBulkRoute(router); + performBulkActionRoute(router, ml); createTimelinesRoute(router, config, security); patchTimelinesRoute(router, config, security); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7f82d9f32a8685..8066ca178d34f8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2879,8 +2879,6 @@ "indexPatternManagement.createIndexPattern.betaLabel": "ベータ", "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", "indexPatternManagement.createIndexPattern.documentation": "ドキュメンテーションを表示", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "この機能にはベーシックライセンスが必要です。", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "新規データを確認", "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "一部のインデックスは表示されない場合があります。{link}してください。", "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "インデックスパターンを作成します", @@ -5245,7 +5243,6 @@ "xpack.apm.agentConfig.chooseService.editButton": "編集", "xpack.apm.agentConfig.chooseService.service.environment.label": "環境", "xpack.apm.agentConfig.chooseService.service.name.label": "サービス名", - "xpack.apm.agentConfig.chooseService.title": "サービスを選択", "xpack.apm.agentConfig.circuitBreakerEnabled.description": "Circuit Breaker を有効にすべきかどうかを指定するブール値。 有効にすると、エージェントは定期的にストレス監視をポーリングして、システム/プロセス/JVMのストレス状態を検出します。監視のいずれかがストレスの兆候を検出した場合、`recording`構成オプションの設定が「false」であるかのようにエージェントは一時停止し、リソース消費を最小限に抑えられます。一時停止した場合、エージェントはストレス状態が緩和されたかどうかを検出するために同じ監視のポーリングを継続します。すべての監視でシステム/プロセス/JVMにストレスがないことが認められると、エージェントは再開して完全に機能します。", "xpack.apm.agentConfig.circuitBreakerEnabled.label": "Cirtcuit Breaker が有効", "xpack.apm.agentConfig.configTable.appliedTooltipMessage": "1 つ以上のエージェントにより適用されました", @@ -5304,8 +5301,6 @@ "xpack.apm.agentConfig.servicePage.service.description": "構成するサービスを選択してください。", "xpack.apm.agentConfig.servicePage.service.fieldLabel": "サービス名", "xpack.apm.agentConfig.servicePage.service.title": "サービス", - "xpack.apm.agentConfig.servicePage.title": "サービスを選択", - "xpack.apm.agentConfig.settings.title": "構成オプション", "xpack.apm.agentConfig.settingsPage.discardChangesButton": "変更を破棄", "xpack.apm.agentConfig.settingsPage.notFound.message": "リクエストされた構成が存在しません", "xpack.apm.agentConfig.settingsPage.notFound.title": "申し訳ございません、エラーが発生しました", @@ -5324,7 +5319,6 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuReliefThreshold.label": "ストレス監視システム CPU 緩和しきい値", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "システムCPU監視でシステムCPUストレスの検出に使用するしきい値。システムCPUが少なくとも「stress_monitor_cpu_duration_threshold」と同じ長さ以上の期間にわたってこのしきい値を超えると、監視機能はこれをストレス状態と見なします。", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "ストレス監視システム CPU ストレスしきい値", - "xpack.apm.agentConfig.titleText": "エージェント中央構成", "xpack.apm.agentConfig.transactionIgnoreUrl.description": "特定の URL への要求が命令されないように制限するために使用します。この構成では、無視される URL パスのワイルドカードパターンのカンマ区切りのリストを使用できます。受信 HTTP 要求が検出されると、要求パスが、リストの各要素に対してテストされます。たとえば、このリストに「/home/index」を追加すると、一致して、「http://localhost/home/index」と「http://whatever.com/home/index?value1=123」から命令が削除されます。", "xpack.apm.agentConfig.transactionIgnoreUrl.label": "URL に基づくトランザクションを無視", "xpack.apm.agentConfig.transactionMaxSpans.description": "トランザクションごとに記録される範囲を制限します。", @@ -5390,9 +5384,6 @@ "xpack.apm.apply.label": "適用", "xpack.apm.applyFilter": "{title} フィルターを適用", "xpack.apm.applyOptions": "オプションを適用", - "xpack.apm.breadcrumb.serviceMapTitle": "サービスマップ", - "xpack.apm.breadcrumb.servicesTitle": "サービス", - "xpack.apm.breadcrumb.tracesTitle": "トレース", "xpack.apm.chart.annotation.version": "バージョン", "xpack.apm.chart.cpuSeries.processAverageLabel": "プロセス平均", "xpack.apm.chart.cpuSeries.processMaxLabel": "プロセス最大", @@ -5785,7 +5776,6 @@ "xpack.apm.settings.anomalyDetection.addEnvironments.selectorLabel": "環境", "xpack.apm.settings.anomalyDetection.addEnvironments.selectorPlaceholder": "環境を選択または追加", "xpack.apm.settings.anomalyDetection.addEnvironments.titleText": "環境を選択", - "xpack.apm.settings.anomalyDetection.descriptionText": "機械学習異常検知統合により、レイテンシの異常を特定することで、各構成された環境で、サービスのアプリケーション正常性ステータスインジケーターが有効になります。", "xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel": "アクション", "xpack.apm.settings.anomalyDetection.jobList.addEnvironments": "MLジョブを作成", "xpack.apm.settings.anomalyDetection.jobList.emptyListText": "異常検知ジョブがありません。", @@ -5795,7 +5785,6 @@ "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText": "異常検知を新しい環境に追加するには、機械学習ジョブを作成します。既存の機械学習ジョブは、{mlJobsLink}で管理できます。", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText": "機械学習", "xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText": "MLでジョブを表示", - "xpack.apm.settings.anomalyDetection.titleText": "異常検知", "xpack.apm.settings.apmIndices.applyButton": "変更を適用", "xpack.apm.settings.apmIndices.applyChanges.failed.text": "インデックスの適用時に何か問題が発生しました。エラー:{errorMessage}", "xpack.apm.settings.apmIndices.applyChanges.failed.title": "インデックスが適用できませんでした。", @@ -5813,8 +5802,6 @@ "xpack.apm.settings.apmIndices.title": "インデックス", "xpack.apm.settings.apmIndices.transactionIndicesLabel": "トランザクションインデックス", "xpack.apm.settings.customizeApp": "アプリをカスタマイズ", - "xpack.apm.settings.customizeApp.description": "次の設定でAPMアプリ経験を拡張します。", - "xpack.apm.settings.customizeApp.title": "アプリをカスタマイズ", "xpack.apm.settings.customizeUI.customLink": "カスタムリンク", "xpack.apm.settings.customizeUI.customLink.create.failed": "リンクを保存できませんでした!", "xpack.apm.settings.customizeUI.customLink.create.failed.message": "リンクを保存するときに問題が発生しました。エラー:「{errorMessage}」", @@ -5863,7 +5850,6 @@ "xpack.apm.settings.customizeUI.customLink.table.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "インデックス", - "xpack.apm.settings.pageTitle": "設定", "xpack.apm.settingsLinkLabel": "設定", "xpack.apm.setupInstructionsButtonLabel": "セットアップの手順", "xpack.apm.significanTerms.license.text": "相関関係APIを使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。", @@ -7017,20 +7003,7 @@ "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "エレメントを更新", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "データを更新", "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF}生成{URL}がクリップボードにコピーされました。", - "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", - "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "'{workpadName}'の{PDF}を作成できませんでした", - "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "{PDF}をエクスポートしています。管理で進捗を確認できます。", - "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "ワークパッド '{workpadName}' の {PDF} エクスポート", - "xpack.canvas.workpadHeaderShareMenu.FullPageLayoutHelpText": "枠線とフッターロゴを削除", - "xpack.canvas.workpadHeaderShareMenu.FullPageLayoutLabel": "全ページレイアウト", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelAdvancedOptionsLabel": "高度なオプション", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL}をクリップボードにコピーするにはEnterキーを押してください。", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "{POST} {URL}をコピー", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "{POST} {URL}をコピーして{KIBANA}外またはWatcherから生成を実行することもできます。", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "{PDF}を生成", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelOptionsLabel": "オプション", "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "{JSON} をダウンロード", "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF}レポート", "xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel": "共有", @@ -14670,8 +14643,6 @@ "xpack.ml.fieldTypeIcon.numberTypeAriaLabel": "数字タイプ", "xpack.ml.fieldTypeIcon.textTypeAriaLabel": "テキストタイプ", "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "不明なタイプ", - "xpack.ml.fileDataVisualizerDescription": "CSV、NDJSON、またはログファイルをインポートします。", - "xpack.ml.fileDataVisualizerTitle": "ファイルをアップロード", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "実際値が通常値と同じ", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "100x よりも高い", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "100x よりも低い", @@ -17895,14 +17866,10 @@ "xpack.reporting.panelContent.generateButtonLabel": "{reportingType} を生成", "xpack.reporting.panelContent.generationTimeDescription": "{objectType} のサイズによって、{reportingType} の作成には数分かかる場合があります。", "xpack.reporting.panelContent.howToCallGenerationDescription": "POST URL をコピーして Kibana 外または ウォッチャー から生成を実行することもできます。", - "xpack.reporting.panelContent.noPermissionToGenerateReportDescription": "このレポートを生成するパーミッションがありません。", - "xpack.reporting.panelContent.notification.cantReachServerDescription": "サーバーと通信できません。再試行してください。", "xpack.reporting.panelContent.notification.reportingErrorTitle": "レポートエラー", "xpack.reporting.panelContent.saveWorkDescription": "レポートの生成前に変更内容を保存してください。", "xpack.reporting.panelContent.successfullyQueuedReportNotificationDescription": "{path}で進捗状況を追跡", "xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle": "{objectType} のレポートキュー", - "xpack.reporting.panelContent.whatCanBeExportedWarningDescription": "初めに変更内容を保存してください", - "xpack.reporting.panelContent.whatCanBeExportedWarningTitle": "保存された {objectType} のみエクスポートできます", "xpack.reporting.pdfFooterImageDescription": "PDFのフッターに使用するカスタム画像です", "xpack.reporting.pdfFooterImageLabel": "PDFフッター画像", "xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportMessage": "レポートには、スプレッドシートアプリケーションで式と解釈される可能性のある文字が含まれています。", @@ -20103,7 +20070,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "ルール監視", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.rules": "ルール", "xpack.securitySolution.detectionEngine.rules.backOptionsHeader": "検出に戻る", - "xpack.securitySolution.detectionEngine.rules.components.genericDownloader.exportFailureTitle": "データをエクスポートできませんでした…", "xpack.securitySolution.detectionEngine.rules.components.ruleActionsOverflow.allActionsTitle": "すべてのアクション", "xpack.securitySolution.detectionEngine.rules.continueButtonTitle": "続行", "xpack.securitySolution.detectionEngine.rules.create.successfullyCreatedRuleTitle": "{ruleName}が作成されました", @@ -21394,7 +21360,6 @@ "xpack.securitySolution.trustedapps.list.actions.delete": "削除", "xpack.securitySolution.trustedapps.list.actions.delete.description": "このエントリを削除", "xpack.securitySolution.trustedapps.list.addButton": "信頼できるアプリケーションを追加", - "xpack.securitySolution.trustedapps.list.backButton": "戻る", "xpack.securitySolution.trustedapps.list.columns.actions": "アクション", "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", "xpack.securitySolution.trustedapps.listEmptyState.message": "現在、エンドポイントには信頼できるアプリケーションがありません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c3c0402ef27ab7..33f5d4aceb51c1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2893,8 +2893,6 @@ "indexPatternManagement.createIndexPattern.betaLabel": "公测版", "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", "indexPatternManagement.createIndexPattern.documentation": "阅读文档", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "此功能需要基本级许可证。", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本级", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "检查新数据", "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "部分索引可能已隐藏。仍然尝试{link}。", "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "创建索引模式", @@ -5274,7 +5272,6 @@ "xpack.apm.agentConfig.chooseService.editButton": "编辑", "xpack.apm.agentConfig.chooseService.service.environment.label": "环境", "xpack.apm.agentConfig.chooseService.service.name.label": "服务名称", - "xpack.apm.agentConfig.chooseService.title": "选择服务", "xpack.apm.agentConfig.circuitBreakerEnabled.description": "指定是否应启用断路器的布尔值。 启用时,代理定期轮询压力监测以检测系统/进程/JVM 压力状态。如果任意监测检测到压力迹象,代理将会暂停,就如 `recording` 配置选项已设置为 `false` 一样,从而使资源消耗降低至最小程度。暂停时,代理继续轮询相同的监测,以便检测压力状态是否已缓解。如果所有监测认为系统/进程/JVM 不再承受压力时,代理将完全恢复正常运行。", "xpack.apm.agentConfig.circuitBreakerEnabled.label": "断路器已启用", "xpack.apm.agentConfig.configTable.appliedTooltipMessage": "已至少由一个代理应用", @@ -5333,8 +5330,6 @@ "xpack.apm.agentConfig.servicePage.service.description": "选择要配置的服务。", "xpack.apm.agentConfig.servicePage.service.fieldLabel": "服务名称", "xpack.apm.agentConfig.servicePage.service.title": "服务", - "xpack.apm.agentConfig.servicePage.title": "选择服务", - "xpack.apm.agentConfig.settings.title": "配置选项", "xpack.apm.agentConfig.settingsPage.discardChangesButton": "放弃更改", "xpack.apm.agentConfig.settingsPage.notFound.message": "请求的配置不存在", "xpack.apm.agentConfig.settingsPage.notFound.title": "抱歉,有错误", @@ -5353,7 +5348,6 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuReliefThreshold.label": "压力监测系统 cpu 缓解阈值", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "系统 CPU 监测用于检测系统 CPU 压力的阈值。如果系统 CPU 超过此阈值的持续时间至少有 `stress_monitor_cpu_duration_threshold`,监测会将其视为压力状态。", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "压力监测系统 cpu 压力阈值", - "xpack.apm.agentConfig.titleText": "代理中央配置", "xpack.apm.agentConfig.transactionIgnoreUrl.description": "用于限制对某些 URL 的请求不被检测。此配置接受应忽略的 URL 路径的通配符模式逗号分隔列表。当监测到传入 HTTP 请求时,会根据此列表中的每个元素测试其请求路径。例如,将 `/home/index` 添加到此列表中后,该元素将匹配并删除 `http://localhost/home/index` 和 `http://whatever.com/home/index?value1=123` 的检测", "xpack.apm.agentConfig.transactionIgnoreUrl.label": "基于 URL 忽略事务", "xpack.apm.agentConfig.transactionMaxSpans.description": "限制每个事务记录的跨度数量。", @@ -5419,9 +5413,6 @@ "xpack.apm.apply.label": "应用", "xpack.apm.applyFilter": "应用 {title} 筛选", "xpack.apm.applyOptions": "应用选项", - "xpack.apm.breadcrumb.serviceMapTitle": "服务地图", - "xpack.apm.breadcrumb.servicesTitle": "服务", - "xpack.apm.breadcrumb.tracesTitle": "追溯", "xpack.apm.chart.annotation.version": "版本", "xpack.apm.chart.cpuSeries.processAverageLabel": "进程平均值", "xpack.apm.chart.cpuSeries.processMaxLabel": "进程最大值", @@ -5819,7 +5810,6 @@ "xpack.apm.settings.anomalyDetection.addEnvironments.selectorLabel": "环境", "xpack.apm.settings.anomalyDetection.addEnvironments.selectorPlaceholder": "选择或添加环境", "xpack.apm.settings.anomalyDetection.addEnvironments.titleText": "选择环境", - "xpack.apm.settings.anomalyDetection.descriptionText": "Machine Learning 异常检测集成通过识别延迟异常来为每个已配置环境中的服务启用应用程序运行状态指标。", "xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel": "操作", "xpack.apm.settings.anomalyDetection.jobList.addEnvironments": "创建 ML 作业", "xpack.apm.settings.anomalyDetection.jobList.emptyListText": "无异常检测作业。", @@ -5829,7 +5819,6 @@ "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText": "要将异常检测添加到新环境,请创建 Machine Learning 作业。现有 Machine Learning 作业可在 {mlJobsLink} 中进行管理。", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText": "Machine Learning", "xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText": "在 ML 中查看作业", - "xpack.apm.settings.anomalyDetection.titleText": "异常检测", "xpack.apm.settings.apmIndices.applyButton": "应用更改", "xpack.apm.settings.apmIndices.applyChanges.failed.text": "应用索引时出现问题。错误:{errorMessage}", "xpack.apm.settings.apmIndices.applyChanges.failed.title": "无法应用索引。", @@ -5847,8 +5836,6 @@ "xpack.apm.settings.apmIndices.title": "索引", "xpack.apm.settings.apmIndices.transactionIndicesLabel": "事务索引", "xpack.apm.settings.customizeApp": "定制应用", - "xpack.apm.settings.customizeApp.description": "使用以下设置扩展 APM 应用体验。", - "xpack.apm.settings.customizeApp.title": "定制应用", "xpack.apm.settings.customizeUI.customLink": "定制链接", "xpack.apm.settings.customizeUI.customLink.create.failed": "链接无法保存!", "xpack.apm.settings.customizeUI.customLink.create.failed.message": "保存链接时出现了问题。错误:“{errorMessage}”", @@ -5897,7 +5884,6 @@ "xpack.apm.settings.customizeUI.customLink.table.noResultFound": "没有“{value}”的结果。", "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "索引", - "xpack.apm.settings.pageTitle": "设置", "xpack.apm.settingsLinkLabel": "设置", "xpack.apm.setupInstructionsButtonLabel": "设置说明", "xpack.apm.significanTerms.license.text": "要使用相关性 API,必须订阅 Elastic 白金级许可证。", @@ -7065,20 +7051,7 @@ "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "刷新元素", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "刷新数据", "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} 已复制到您的剪贴板。", - "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "已将报告配置复制到剪贴板", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "已将共享标记复制到剪贴板", - "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", - "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", - "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", - "xpack.canvas.workpadHeaderShareMenu.FullPageLayoutHelpText": "删除边框和页脚徽标", - "xpack.canvas.workpadHeaderShareMenu.FullPageLayoutLabel": "全页面布局", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelAdvancedOptionsLabel": "高级选项", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用此 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "生成 {PDF}", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "{PDF} 可能会花费一两分钟生成,具体取决于 Workpad 的大小。", - "xpack.canvas.workpadHeaderShareMenu.pdfPanelOptionsLabel": "选项", "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "下载为 {JSON}", "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} 报告", "xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel": "共享", @@ -14863,8 +14836,6 @@ "xpack.ml.fieldTypeIcon.numberTypeAriaLabel": "数字类型", "xpack.ml.fieldTypeIcon.textTypeAriaLabel": "文本类型", "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "未知类型", - "xpack.ml.fileDataVisualizerDescription": "导入您自己的 CSV、NDJSON 或日志文件。", - "xpack.ml.fileDataVisualizerTitle": "上传文件", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "实际上与典型模式相同", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "高 100 多倍", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "低 100 多倍", @@ -18136,14 +18107,10 @@ "xpack.reporting.panelContent.generateButtonLabel": "生成 {reportingType}", "xpack.reporting.panelContent.generationTimeDescription": "{reportingType} 可能会花费 1 或 2 分钟生成,取决于 {objectType} 的大小。", "xpack.reporting.panelContent.howToCallGenerationDescription": "或者,复制此 POST URL 以从 Kibana 外部或从 Watcher 调用生成。", - "xpack.reporting.panelContent.noPermissionToGenerateReportDescription": "您无权生成此报告。", - "xpack.reporting.panelContent.notification.cantReachServerDescription": "无法访问服务器。请重试。", "xpack.reporting.panelContent.notification.reportingErrorTitle": "报告错误", "xpack.reporting.panelContent.saveWorkDescription": "请在生成报告之前保存您的工作。", "xpack.reporting.panelContent.successfullyQueuedReportNotificationDescription": "在 {path} 中跟踪其进度", "xpack.reporting.panelContent.successfullyQueuedReportNotificationTitle": "已为 {objectType} 排队报告", - "xpack.reporting.panelContent.whatCanBeExportedWarningDescription": "请先保存您的工作", - "xpack.reporting.panelContent.whatCanBeExportedWarningTitle": "只会导出保存的 {objectType}", "xpack.reporting.pdfFooterImageDescription": "要在 PDF 的页脚中使用的定制图像", "xpack.reporting.pdfFooterImageLabel": "PDF 页脚图像", "xpack.reporting.publicNotifier.csvContainsFormulas.formulaReportMessage": "报告包含电子表格应用程序可解释为公式的字符。", @@ -20388,13 +20355,11 @@ "xpack.securitySolution.detectionEngine.rules.allRules.showingExceptionLists": "正在显示 {totalLists} 个{totalLists, plural, other {列表}}", "xpack.securitySolution.detectionEngine.rules.allRules.showingRulesTitle": "正在显示 {totalRules} 个{totalRules, plural, other {规则}}", "xpack.securitySolution.detectionEngine.rules.allRules.successfullyDuplicatedRulesTitle": "已成功复制 {totalRules, plural, other {{totalRules} 个规则}}", - "xpack.securitySolution.detectionEngine.rules.allRules.successfullyExportedRulesTitle": "已成功导出{totalRules, plural, =0 {所有规则} other { {totalRules} 个规则}}", "xpack.securitySolution.detectionEngine.rules.allRules.tableTitle": "所有规则", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.exceptions": "例外列表", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.monitoring": "规则监测", "xpack.securitySolution.detectionEngine.rules.allRules.tabs.rules": "规则", "xpack.securitySolution.detectionEngine.rules.backOptionsHeader": "返回到检测", - "xpack.securitySolution.detectionEngine.rules.components.genericDownloader.exportFailureTitle": "无法导出数据……", "xpack.securitySolution.detectionEngine.rules.components.ruleActionsOverflow.allActionsTitle": "所有操作", "xpack.securitySolution.detectionEngine.rules.continueButtonTitle": "继续", "xpack.securitySolution.detectionEngine.rules.create.successfullyCreatedRuleTitle": "{ruleName} 已创建", @@ -21730,7 +21695,6 @@ "xpack.securitySolution.trustedapps.list.actions.delete": "移除", "xpack.securitySolution.trustedapps.list.actions.delete.description": "移除此条目", "xpack.securitySolution.trustedapps.list.addButton": "添加受信任的应用程序", - "xpack.securitySolution.trustedapps.list.backButton": "返回", "xpack.securitySolution.trustedapps.list.columns.actions": "操作", "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, other {# 个受信任的应用程序}}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 20aec6974d395b..3da25cb4cf0d8d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -90,7 +90,7 @@ export const TriggersActionsUIHome: React.FunctionComponent - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 1bbffc850ee185..cad4dabbe8275d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -12,8 +12,14 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { act } from '@testing-library/react'; import { AlertDetails } from './alert_details'; import { Alert, ActionType, AlertTypeModel, AlertType } from '../../../../types'; -import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui'; -import { ViewInApp } from './view_in_app'; +import { + EuiBadge, + EuiFlexItem, + EuiSwitch, + EuiButtonEmpty, + EuiText, + EuiPageHeaderProps, +} from '@elastic/eui'; import { ActionGroup, AlertExecutionStatusErrorReasons, @@ -75,13 +81,7 @@ describe('alert_details', () => { expect( shallow( - ).containsMatchingElement( - -

- {alert.name} -

-
- ) + ).find('EuiPageHeader') ).toBeTruthy(); }); @@ -103,7 +103,7 @@ describe('alert_details', () => { expect( shallow( - ).containsMatchingElement({alertType.name}) + ).find({alertType.name}) ).toBeTruthy(); }); @@ -290,7 +290,7 @@ describe('alert_details', () => { expect( shallow( - ).containsMatchingElement() + ).find('ViewInApp') ).toBeTruthy(); }); @@ -309,16 +309,29 @@ describe('alert_details', () => { minimumLicenseRequired: 'basic', enabledInLicense: true, }; - - expect( - shallow( - - ) - .find(EuiButtonEmpty) - .find('[data-test-subj="openEditAlertFlyoutButton"]') - .first() - .exists() - ).toBeTruthy(); + const pageHeaderProps = shallow( + + ) + .find('EuiPageHeader') + .props() as EuiPageHeaderProps; + const rightSideItems = pageHeaderProps.rightSideItems; + expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + + + + + + `); }); }); }); @@ -768,20 +781,34 @@ describe('edit button', () => { enabledInLicense: true, }; - expect( - shallow( - + ) + .find('EuiPageHeader') + .props() as EuiPageHeaderProps; + const rightSideItems = pageHeaderProps.rightSideItems; + expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + + + - ) - .find(EuiButtonEmpty) - .find('[name="edit"]') - .first() - .exists() - ).toBeTruthy(); + + + `); }); it('should not render an edit button when alert editable but actions arent', () => { @@ -851,20 +878,34 @@ describe('edit button', () => { enabledInLicense: true, }; - expect( - shallow( - + ) + .find('EuiPageHeader') + .props() as EuiPageHeaderProps; + const rightSideItems = pageHeaderProps.rightSideItems; + expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + + + - ) - .find(EuiButtonEmpty) - .find('[name="edit"]') - .first() - .exists() - ).toBeTruthy(); + + + `); }); }); @@ -885,7 +926,7 @@ describe('refresh button', () => { }; const requestRefresh = jest.fn(); - const refreshButton = shallow( + const wrapper = mountWithIntl( { {...mockAlertApis} requestRefresh={requestRefresh} /> - ) - .find('[data-test-subj="refreshAlertsButton"]') - .first(); + ); + const refreshButton = wrapper.find('[data-test-subj="refreshAlertsButton"]').first(); expect(refreshButton.exists()).toBeTruthy(); refreshButton.simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index ba6c77a4403f1d..3e411913520adc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -12,14 +12,11 @@ import { useHistory } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiTitle, + EuiPageHeader, EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, - EuiPage, EuiPageContentBody, EuiSwitch, EuiCallOut, @@ -45,7 +42,7 @@ import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translat import { useKibana } from '../../../../common/lib/kibana'; import { alertReducer } from '../../alert_form/alert_reducer'; -type AlertDetailsProps = { +export type AlertDetailsProps = { alert: Alert; alertType: AlertType; actionTypes: ActionType[]; @@ -120,249 +117,245 @@ export const AlertDetails: React.FunctionComponent = ({ } }; + const rightPageHeaderButtons = hasEditButton + ? [ + <> + setEditFlyoutVisibility(true)} + name="edit" + disabled={!alertType.enabledInLicense} + > + + + {editFlyoutVisible && ( + { + setInitialAlert(alert); + setEditFlyoutVisibility(false); + }} + actionTypeRegistry={actionTypeRegistry} + alertTypeRegistry={alertTypeRegistry} + onSave={setAlert} + /> + )} + , + ] + : []; + return ( - - - - - - -

- {alert.name} -

-
-
- - - {hasEditButton ? ( - - <> - {' '} - setEditFlyoutVisibility(true)} - name="edit" - disabled={!alertType.enabledInLicense} - > - - - {editFlyoutVisible && ( - { - setInitialAlert(alert); - setEditFlyoutVisibility(false); - }} - actionTypeRegistry={actionTypeRegistry} - alertTypeRegistry={alertTypeRegistry} - onSave={setAlert} - /> - )} - - - ) : null} + + + + } + rightSideItems={[ + , + + + , + ...rightPageHeaderButtons, + ]} + /> + + + + + +

+ +

+
+ + {alertType.name} +
+ + {uniqueActions && uniqueActions.length ? ( + <> + +

+ +

+
+ + + {uniqueActions.map((action, index) => ( + + + {actionTypesByTypeId[action].name ?? action} + + + ))} + + + ) : null} +
+ + + - - - + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(alert); + // Reset dismiss if previously clicked + setDissmissAlertErrors(false); + } else { + setIsEnabled(true); + await enableAlert(alert); + } + requestRefresh(); + }} + label={ + + } + /> - + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + requestRefresh(); + }} + label={ + + } + /> -
-
- - - - -

- -

-
- - {alertType.name} -
- - {uniqueActions && uniqueActions.length ? ( - <> - -

- -

-
- - - {uniqueActions.map((action, index) => ( - - - {actionTypesByTypeId[action].name ?? action} - - - ))} - - - ) : null} -
- - - - - { - if (isEnabled) { - setIsEnabled(false); - await disableAlert(alert); - // Reset dismiss if previously clicked - setDissmissAlertErrors(false); - } else { - setIsEnabled(true); - await enableAlert(alert); - } - requestRefresh(); - }} - label={ - - } - /> - - - { - if (isMuted) { - setIsMuted(false); - await unmuteAlert(alert); - } else { - setIsMuted(true); - await muteAlert(alert); - } - requestRefresh(); - }} - label={ + + + {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( + + + + + {alert.executionStatus.error?.message} + + + + + setDissmissAlertErrors(true)} + > - } - /> - - - - - {alert.enabled && !dissmissAlertErrors && alert.executionStatus.status === 'error' ? ( - - - - - {alert.executionStatus.error?.message} - - - + + + {alert.executionStatus.error?.reason === + AlertExecutionStatusErrorReasons.License && ( - setDissmissAlertErrors(true)} + target="_blank" > - + - {alert.executionStatus.error?.reason === - AlertExecutionStatusErrorReasons.License && ( - - - - - - )} - - - -
- ) : null} - - - {alert.enabled ? ( - - ) : ( - <> - - -

- -

-
- - )} + )} +
+
- -
- - + ) : null} + + + {alert.enabled ? ( + + ) : ( + <> + + +

+ +

+
+ + )} +
+
+ +
+ ); }; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index e02cf44b0856eb..c34da1bc097bc0 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -12,7 +12,8 @@ import { PluginInitializerContext, AppMountParameters, } from 'kibana/public'; -import { of } from 'rxjs'; +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -104,32 +105,41 @@ export class UptimePlugin }); plugins.observability.navigation.registerSections( - of([ - { - label: 'Uptime', - sortKey: 200, - entries: [ - { - label: i18n.translate('xpack.uptime.overview.heading', { - defaultMessage: 'Monitoring overview', - }), - app: 'uptime', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - { - label: i18n.translate('xpack.uptime.certificatesPage.heading', { - defaultMessage: 'TLS Certificates', - }), - app: 'uptime', - path: '/certificates', - matchFullPath: true, - }, - ], - }, - ]) + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Uptime', + sortKey: 200, + entries: [ + { + label: i18n.translate('xpack.uptime.overview.heading', { + defaultMessage: 'Monitoring overview', + }), + app: 'uptime', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + { + label: i18n.translate('xpack.uptime.certificatesPage.heading', { + defaultMessage: 'TLS Certificates', + }), + app: 'uptime', + path: '/certificates', + matchFullPath: true, + }, + ], + }, + ]; + } + + return []; + }) + ) ); + core.application.register({ id: PLUGIN.ID, euiIconType: 'logoObservability', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 40c0fe398bc57e..c1f6bcb9e15100 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -149,13 +149,17 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`); + validateInstanceEvent(event, `created new instance: 'instance'`, false); break; case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`); + validateInstanceEvent(event, `instance 'instance' has recovered`, true); break; case 'active-instance': - validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup: 'default'`, + false + ); break; // this will get triggered as we add new event actions default: @@ -163,7 +167,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } } - function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { spaceId: Spaces.space1.id, savedObjects: [ @@ -172,6 +180,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, }); } }); @@ -288,10 +297,10 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`); + validateInstanceEvent(event, `created new instance: 'instance'`, false); break; case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`); + validateInstanceEvent(event, `instance 'instance' has recovered`, true); break; case 'active-instance': expect( @@ -299,7 +308,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ).to.be(true); validateInstanceEvent( event, - `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'` + `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, + false ); break; // this will get triggered as we add new event actions @@ -308,7 +318,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } } - function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { spaceId: Spaces.space1.id, savedObjects: [ @@ -317,6 +331,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, }); } }); @@ -376,6 +391,7 @@ interface ValidateEventLogParams { savedObjects: SavedObject[]; outcome?: string; message: string; + shouldHaveEventEnd?: boolean; errorMessage?: string; status?: string; actionGroupId?: string; @@ -385,7 +401,7 @@ interface ValidateEventLogParams { export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { const { spaceId, savedObjects, outcome, message, errorMessage } = params; - const { status, actionGroupId, instanceId, reason } = params; + const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params; if (status) { expect(event?.kibana?.alerting?.status).to.be(status); @@ -411,16 +427,23 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa if (duration !== undefined) { expect(typeof duration).to.be('number'); expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); - const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) - ); + if (shouldHaveEventEnd !== false) { + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); - // account for rounding errors - expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + } + + if (shouldHaveEventEnd === false) { + expect(eventEnd).not.to.be.ok(); + } } expect(event?.event?.outcome).to.equal(outcome); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts new file mode 100644 index 00000000000000..2b92562b9bde5d --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +// eslint-disable-next-line import/no-default-export +export default function eventLogAlertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('eventLog alerts', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should generate expected alert events for normal operation', async () => { + // pattern of when the alert should fire + const pattern = { + instance: [false, true, true, false, false, true, true, true], + }; + + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [], + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); + + // wait for the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute', { gte: 9 }], + ['new-instance', { equal: 2 }], + ['active-instance', { gte: 4 }], + ['recovered-instance', { equal: 2 }], + ]), + }); + }); + + // filter out the execute event actions + const instanceEvents = events.filter( + (event: IValidatedEvent) => event?.event?.action !== 'execute' + ); + + const currentAlertSpan: { + alertId?: string; + start?: string; + durationToDate?: number; + } = {}; + for (let i = 0; i < instanceEvents.length; ++i) { + switch (instanceEvents[i]?.event?.action) { + case 'new-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + // a new alert should generate a unique UUID for the duration of its activeness + expect(instanceEvents[i]?.event?.end).to.be(undefined); + + currentAlertSpan.alertId = instanceEvents[i]?.kibana?.alerting?.instance_id; + currentAlertSpan.start = instanceEvents[i]?.event?.start; + currentAlertSpan.durationToDate = instanceEvents[i]?.event?.duration; + break; + + case 'active-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); + expect(instanceEvents[i]?.event?.end).to.be(undefined); + + if (instanceEvents[i]?.event?.duration! !== 0) { + expect(instanceEvents[i]?.event?.duration! > currentAlertSpan.durationToDate!).to.be( + true + ); + } + currentAlertSpan.durationToDate = instanceEvents[i]?.event?.duration; + break; + + case 'recovered-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); + expect(instanceEvents[i]?.event?.end).not.to.be(undefined); + expect( + new Date(instanceEvents[i]?.event?.end!).valueOf() - + new Date(instanceEvents[i]?.event?.start!).valueOf() + ).to.equal(instanceEvents[i]?.event?.duration! / 1000 / 1000); + break; + } + } + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 9154c85af1bc7a..318dfdfe065dfe 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -76,7 +76,9 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); alertInstances.forEach(([key, value], index) => { expect(key).to.eql(`instance-${index}`); - expect(value.state).to.eql({ instanceStateValue: true }); + expect(value.state.instanceStateValue).to.be(true); + expect(value.state.start).not.to.be(undefined); + expect(value.state.duration).not.to.be(undefined); }); }); @@ -131,7 +133,9 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); alertInstances.forEach(([key, value], index) => { expect(key).to.eql(`instance-${index}`); - expect(value.state).to.eql({ instanceStateValue: true }); + expect(value.state.instanceStateValue).to.be(true); + expect(value.state.start).not.to.be(undefined); + expect(value.state.duration).not.to.be(undefined); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index e9aeec1717c968..5c3374a4d9c704 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -37,6 +37,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./builtin_alert_types')); loadTestFile(require.resolve('./mustache_templates.ts')); loadTestFile(require.resolve('./notify_when')); + loadTestFile(require.resolve('./event_log_alerts')); // note that this test will destroy existing spaces loadTestFile(require.resolve('./migrations')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts index 323b1b377e5555..0e92456b66c859 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/patch_configure.ts @@ -8,14 +8,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { - getConfigurationRequest, removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, deleteConfiguration, createConfiguration, updateConfiguration, + getConfigurationRequest, + getConfiguration, } from '../../../../common/lib/utils'; import { secOnly, @@ -52,6 +54,39 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); }); + it('should update mapping when changing connector', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration(supertest, configuration.id, { + connector: { + id: 'serviceNowITSM', + name: 'ServiceNow ITSM', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + version: configuration.version, + }); + const newConfiguration = await getConfiguration({ supertest }); + + expect(configuration.mappings).to.eql([]); + expect(newConfiguration[0].mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ]); + }); + it('should not patch a configuration with unsupported connector type', async () => { const configuration = await createConfiguration(supertest); await updateConfiguration( diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts index 44ec24f688f201..fd9e8611db44ad 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/post_configure.ts @@ -60,20 +60,135 @@ export default ({ getService }: FtrProviderContext): void => { expect(configuration.length).to.be(1); }); - it('should return an error when failing to get mapping', async () => { + it('should return an empty mapping when they type is none', async () => { const postRes = await createConfiguration( supertest, getConfigurationRequest({ id: 'not-exists', name: 'not-exists', - type: ConnectorTypes.jira, + type: ConnectorTypes.none, }) ); - expect(postRes.error).to.not.be(null); expect(postRes.mappings).to.eql([]); }); + it('should return the correct mapping for Jira', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'summary', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'comments', + }, + ]); + }); + + it('should return the correct mapping for IBM Resilient', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'resilient', + name: 'Resilient', + type: ConnectorTypes.resilient, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'name', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'comments', + }, + ]); + }); + + it('should return the correct mapping for ServiceNow ITSM', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'serviceNowITSM', + name: 'ServiceNow ITSM', + type: ConnectorTypes.serviceNowITSM, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ]); + }); + + it('should return the correct mapping for ServiceNow SecOps', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'serviceNowSIR', + name: 'ServiceNow SecOps', + type: ConnectorTypes.serviceNowSIR, + }) + ); + + expect(postRes.mappings).to.eql([ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ]); + }); + it('should not create a configuration when missing connector.id', async () => { await createConfiguration( supertest, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index 491f7bb9c417e5..2294d51537fb13 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -23,6 +23,7 @@ import { deleteListsIndex, importFile, } from '../../../lists_api_integration/utils'; +import { SIGNALS_TEMPLATE_VERSION } from '../../../../plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -127,7 +128,7 @@ export default ({ getService }: FtrProviderContext) => { host: { name: ['mothra'] }, event: { kind: 'signal' }, signal: { - _meta: { version: 35 }, + _meta: { version: SIGNALS_TEMPLATE_VERSION }, parents: [ { id: diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 755847e8b645d8..e6a835462619c1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -27,6 +27,7 @@ import { import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; +import { SIGNALS_TEMPLATE_VERSION } from '../../../../plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template'; const format = (value: unknown): string => JSON.stringify(value, null, 2); @@ -201,7 +202,7 @@ export default ({ getService }: FtrProviderContext) => { }, signal: { _meta: { - version: 35, + version: SIGNALS_TEMPLATE_VERSION, }, ancestors: [ { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts index 0c274a8f4678bb..d5f287e78268eb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts @@ -47,8 +47,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 - describe.skip('Finalizing signals migrations', () => { + describe('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 0ba0749e75b08e..c3dbd24ae9f046 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -853,7 +853,7 @@ export default ({ getService }: FtrProviderContext) => { 'host.id': '8cc95778cce5407c809480e8e32ad76b', event: { kind: 'signal' }, signal: { - _meta: { version: 35 }, + _meta: { version: SIGNALS_TEMPLATE_VERSION }, parents: [ { depth: 0, @@ -1011,7 +1011,7 @@ export default ({ getService }: FtrProviderContext) => { 'host.id': '8cc95778cce5407c809480e8e32ad76b', event: { kind: 'signal' }, signal: { - _meta: { version: 35 }, + _meta: { version: SIGNALS_TEMPLATE_VERSION }, parents: [ { depth: 0, @@ -1101,7 +1101,7 @@ export default ({ getService }: FtrProviderContext) => { 'process.name': 'sshd', event: { kind: 'signal' }, signal: { - _meta: { version: 35 }, + _meta: { version: SIGNALS_TEMPLATE_VERSION }, parents: [ { depth: 0, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts index 6bb00c55f690e1..5c726a177bb601 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts @@ -19,8 +19,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 - describe.skip('Signals migration status', () => { + describe('Signals migration status', () => { let legacySignalsIndexName: string; beforeEach(async () => { await createSignalsIndex(supertest); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index fd3675a2e47e62..3c5e04ee1f64ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -35,6 +35,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./update_rules')); loadTestFile(require.resolve('./update_rules_bulk')); loadTestFile(require.resolve('./patch_rules_bulk')); + loadTestFile(require.resolve('./perform_bulk_action')); loadTestFile(require.resolve('./patch_rules')); loadTestFile(require.resolve('./read_privileges')); loadTestFile(require.resolve('./query_signals')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts new file mode 100644 index 00000000000000..53613624067e16 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { + DETECTION_ENGINE_RULES_BULK_ACTION, + DETECTION_ENGINE_RULES_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { BulkAction } from '../../../../plugins/security_solution/common/detection_engine/schemas/common/schemas'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + binaryToString, + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('perform_bulk_action', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + }); + + it('should export rules', async () => { + await createRule(supertest, getSimpleRule()); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_ACTION) + .set('kbn-xsrf', 'true') + .send({ query: '', action: BulkAction.export }) + .expect(200) + .expect('Content-Type', 'application/ndjson') + .expect('Content-Disposition', 'attachment; filename="rules_export.ndjson"') + .parse(binaryToString); + + const [ruleJson, exportDetailsJson] = body.toString().split(/\n/); + + const rule = removeServerGeneratedProperties(JSON.parse(ruleJson)); + expect(rule).to.eql(getSimpleRuleOutput()); + + const exportDetails = JSON.parse(exportDetailsJson); + expect(exportDetails).to.eql({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + }); + }); + + it('should delete rules', async () => { + const ruleId = 'ruleId'; + await createRule(supertest, getSimpleRule(ruleId)); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_ACTION) + .set('kbn-xsrf', 'true') + .send({ query: '', action: BulkAction.delete }) + .expect(200); + + expect(body).to.eql({ success: true, rules_count: 1 }); + + await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`) + .set('kbn-xsrf', 'true') + .expect(404); + }); + + it('should enable rules', async () => { + const ruleId = 'ruleId'; + await createRule(supertest, getSimpleRule(ruleId)); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_ACTION) + .set('kbn-xsrf', 'true') + .send({ query: '', action: BulkAction.enable }) + .expect(200); + + expect(body).to.eql({ success: true, rules_count: 1 }); + + const { body: ruleBody } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const referenceRule = getSimpleRuleOutput(ruleId); + referenceRule.enabled = true; + + const storedRule = removeServerGeneratedProperties(ruleBody); + + expect(storedRule).to.eql(referenceRule); + }); + + it('should disable rules', async () => { + const ruleId = 'ruleId'; + await createRule(supertest, getSimpleRule(ruleId, true)); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_ACTION) + .set('kbn-xsrf', 'true') + .send({ query: '', action: BulkAction.disable }) + .expect(200); + + expect(body).to.eql({ success: true, rules_count: 1 }); + + const { body: ruleBody } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=${ruleId}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const referenceRule = getSimpleRuleOutput(ruleId); + const storedRule = removeServerGeneratedProperties(ruleBody); + + expect(storedRule).to.eql(referenceRule); + }); + + it('should duplicate rules', async () => { + const ruleId = 'ruleId'; + await createRule(supertest, getSimpleRule(ruleId)); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_ACTION) + .set('kbn-xsrf', 'true') + .send({ query: '', action: BulkAction.duplicate }) + .expect(200); + + expect(body).to.eql({ success: true, rules_count: 1 }); + + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(rulesResponse.total).to.eql(2); + }); + }); +}; diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index f048bf47991f2f..f499c5bf0cfe8a 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -143,6 +143,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(styleObj['background-color']).to.be('rgb(168, 191, 218)'); // should also set text color when in cell mode expect(styleObj.color).to.be('rgb(0, 0, 0)'); + await PageObjects.lens.closeTablePalettePanel(); + }); + + it('should allow to show a summary table for metric columns', async () => { + await PageObjects.lens.setTableSummaryRowFunction('sum'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertExactText( + '[data-test-subj="lnsDataTable-footer-169.228.188.120-›-Average-of-bytes"]', + 'Sum: 18,994' + ); }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index bbc7f5992506b3..18f4f6a38a7b10 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -35,14 +35,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 280801d1becb5b..431c0550b92718 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -41,16 +41,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.error.expectForbidden(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should not display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.navigateToKibanaHome(); await ml.navigation.openKibanaNav(); await ml.testExecution.logTestStep('should not display the ML nav link'); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index dbf467e998f251..a53ed2fafe30c7 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -35,14 +35,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index b8f1e6b3dd2369..c0111afad28932 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -761,6 +761,20 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return buttonEl.click(); }, + async setTableSummaryRowFunction( + summaryFunction: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max' + ) { + await testSubjects.click('lnsDatatable_summaryrow_function'); + await testSubjects.click('lns-datatable-summary-' + summaryFunction); + }, + + async setTableSummaryRowLabel(newLabel: string) { + await testSubjects.setValue('lnsDatatable_summaryrow_label', newLabel, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + }, + async setTableDynamicColoring(coloringType: 'none' | 'cell' | 'text') { await testSubjects.click('lnsDatatable_dynamicColoring_groups_' + coloringType); }, @@ -769,6 +783,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lnsDatatable_dynamicColoring_trigger'); }, + async closeTablePalettePanel() { + await testSubjects.click('lns-indexPattern-PalettePanelContainerBack'); + }, + // different picker from the next one async changePaletteTo(paletteName: string) { await testSubjects.click('lnsDatatable_dynamicColoring_palette_picker'); diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index e8999999ce50b6..742d41031004bc 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -91,7 +91,7 @@ export class ReportingPageObject extends FtrService { } async getQueueReportError() { - return await this.testSubjects.exists('queueReportError'); + return await this.testSubjects.exists('errorToastMessage'); } async getGenerateReportButton() { diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 31cf17575bdd9a..2de5d83714aeed 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -98,14 +98,6 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte }); }, - async assertKibanaHomeFileDataVisLinkExists() { - await testSubjects.existOrFail('homeSynopsisLinkml_file_data_visualizer'); - }, - - async assertKibanaHomeFileDataVisLinkNotExists() { - await testSubjects.missingOrFail('homeSynopsisLinkml_file_data_visualizer'); - }, - async assertRadioGroupValue(testSubject: string, expectedValue: string) { const assertRadioGroupValue = await testSubjects.find(testSubject); const input = await assertRadioGroupValue.findByCssSelector(':checked'); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 7fdfbb45269c30..aff1402f5567ee 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -57,14 +57,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts index 91a37d0d98cda3..8d3aa3c6b6ada7 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts @@ -39,16 +39,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.error.expectForbidden(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should not display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.navigateToKibanaHome(); await ml.navigation.openKibanaNav(); await ml.testExecution.logTestStep('should not display the ML nav link'); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index e58e46e985fd9a..2e5216d7225186 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -58,14 +58,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 34e08ad257f849..8cf92f77d939c7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -136,7 +136,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the alert details', async () => { const headingText = await pageObjects.alertDetailsUI.getHeadingText(); - expect(headingText).to.be(`test-alert-${testRunUuid}`); + expect(headingText.includes(`test-alert-${testRunUuid}`)).to.be(true); const alertType = await pageObjects.alertDetailsUI.getAlertType(); expect(alertType).to.be(`Always Firing`); @@ -279,7 +279,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`); const headingText = await pageObjects.alertDetailsUI.getHeadingText(); - expect(headingText).to.be(updatedAlertName); + expect(headingText.includes(updatedAlertName)).to.be(true); }); it('should reset alert when canceling an edit', async () => { diff --git a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js index 83a759abe337d7..00fce236b5d172 100644 --- a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js +++ b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @@ -227,7 +227,6 @@ export default ({ getService, getPageObjects }) => { url: process.env.TEST_KIBANA_URLDATA, certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), uiSettingDefaults: kibanaServer.uiSettings, - importExportDir: config.get('kbnArchiver.directory'), }); const esArchiver = new EsArchiver({ diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js index d86debceb0d1c5..2b25d5ffea6e1a 100644 --- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js +++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) { describe('check metricbeat Dashboard', function () { before(async function () { - await esArchiver.load('metricbeat'); + await esArchiver.load('../integration-test/test/es_archives/metricbeat'); // this navigateToActualURL takes the place of navigating to the dashboard landing page, // filtering on the dashboard name, selecting it, setting the timepicker, and going to full screen @@ -44,6 +44,10 @@ export default function ({ getService, getPageObjects, updateBaselines }) { await browser.setScreenshotSize(1000, 1000); }); + after(async function () { + await esArchiver.unload('../integration-test/test/es_archives/metricbeat'); + }); + it('[Metricbeat System] Overview ECS should match snapshot', async function () { try { const percentDifference = await screenshot.compareAgainstBaseline( diff --git a/yarn.lock b/yarn.lock index 3afbffbac6eb1b..bd34c0c4cb4b85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,7 +2759,7 @@ version "0.0.0" uid "" -"@kbn/server-route-repository@link:packages/kbn-server-route-repository": +"@kbn/server-route-repository@link:bazel-bin/packages/kbn-server-route-repository": version "0.0.0" uid "" @@ -29273,16 +29273,16 @@ write@1.0.3: mkdirp "^0.5.1" ws@^6.1.2, ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== dependencies: async-limiter "~1.0.0" ws@^7.2.3: - version "7.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + version "7.4.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" + integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== x-is-function@^1.0.4: version "1.0.4"