diff --git a/api_docs/actions.json b/api_docs/actions.json index fb9bafd6def94..ec2bd86581f32 100644 --- a/api_docs/actions.json +++ b/api_docs/actions.json @@ -127,7 +127,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 70 + "lineNumber": 63 } }, { @@ -138,7 +138,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 71 + "lineNumber": 64 } }, { @@ -149,7 +149,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 72 + "lineNumber": 65 } }, { @@ -160,7 +160,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 73 + "lineNumber": 66 }, "signature": [ "Config | undefined" @@ -174,13 +174,13 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 74 + "lineNumber": 67 } } ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 69 + "lineNumber": 62 }, "initialIsOpen": false }, @@ -199,7 +199,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 47 + "lineNumber": 40 }, "signature": [ "() => ", @@ -220,7 +220,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 48 + "lineNumber": 41 }, "signature": [ "() => ", @@ -237,7 +237,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 46 + "lineNumber": 39 }, "initialIsOpen": false }, @@ -256,7 +256,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 56 + "lineNumber": 49 }, "signature": [ { @@ -276,7 +276,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 57 + "lineNumber": 50 }, "signature": [ { @@ -291,7 +291,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 55 + "lineNumber": 48 }, "initialIsOpen": false }, @@ -320,7 +320,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 108 + "lineNumber": 101 } }, { @@ -331,7 +331,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 109 + "lineNumber": 102 } }, { @@ -342,7 +342,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 110 + "lineNumber": 103 }, "signature": [ "number | undefined" @@ -356,7 +356,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 111 + "lineNumber": 104 }, "signature": [ "\"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\"" @@ -370,7 +370,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 112 + "lineNumber": 105 }, "signature": [ "{ params?: ValidatorType | undefined; config?: ValidatorType | undefined; secrets?: ValidatorType | undefined; } | undefined" @@ -395,7 +395,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 117 + "lineNumber": 110 } }, { @@ -408,7 +408,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 117 + "lineNumber": 110 } } ], @@ -416,7 +416,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 117 + "lineNumber": 110 } }, { @@ -427,7 +427,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 118 + "lineNumber": 111 }, "signature": [ { @@ -443,7 +443,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 102 + "lineNumber": 95 }, "initialIsOpen": false }, @@ -472,7 +472,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 62 + "lineNumber": 55 } }, { @@ -483,7 +483,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 63 + "lineNumber": 56 }, "signature": [ { @@ -503,7 +503,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 64 + "lineNumber": 57 }, "signature": [ "Config" @@ -517,7 +517,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 65 + "lineNumber": 58 }, "signature": [ "Secrets" @@ -531,7 +531,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 66 + "lineNumber": 59 }, "signature": [ "Params" @@ -540,7 +540,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 61 + "lineNumber": 54 }, "initialIsOpen": false }, @@ -577,7 +577,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 81 + "lineNumber": 74 }, "signature": [ "Secrets" @@ -586,7 +586,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 77 + "lineNumber": 70 }, "initialIsOpen": false } @@ -1008,7 +1008,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 86 + "lineNumber": 85 } } ], @@ -1016,13 +1016,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 80 + "lineNumber": 79 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 79 + "lineNumber": 78 }, "lifecycle": "setup", "initialIsOpen": true @@ -1053,7 +1053,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } }, { @@ -1071,13 +1071,13 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } } ], @@ -1085,7 +1085,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } }, { @@ -1107,7 +1107,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 93 + "lineNumber": 92 } }, { @@ -1120,7 +1120,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 94 + "lineNumber": 93 } }, { @@ -1138,13 +1138,13 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 95 + "lineNumber": 94 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 95 + "lineNumber": 94 } } ], @@ -1152,7 +1152,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 92 + "lineNumber": 91 } }, { @@ -1197,7 +1197,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 97 + "lineNumber": 96 } } ], @@ -1205,7 +1205,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 97 + "lineNumber": 96 } }, { @@ -1250,7 +1250,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 98 + "lineNumber": 97 } } ], @@ -1258,7 +1258,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 98 + "lineNumber": 97 } }, { @@ -1269,7 +1269,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 99 + "lineNumber": 98 }, "signature": [ { @@ -1301,7 +1301,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 101 + "lineNumber": 100 } }, { @@ -1314,7 +1314,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 102 + "lineNumber": 101 } }, { @@ -1327,7 +1327,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 103 + "lineNumber": 102 } } ], @@ -1335,13 +1335,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 100 + "lineNumber": 99 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 90 + "lineNumber": 89 }, "lifecycle": "start", "initialIsOpen": true diff --git a/api_docs/case.mdx b/api_docs/case.mdx deleted file mode 100644 index 00a5d3c04060b..0000000000000 --- a/api_docs/case.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -id: kibCasePluginApi -slug: /kibana-dev-docs/casePluginApi -title: case -image: https://source.unsplash.com/400x175/?github -summary: API docs for the case plugin -date: 2020-11-16 -tags: ['contributor', 'dev', 'apidocs', 'kibana', 'case'] -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 caseObj from './case.json'; - -## Server - -### Interfaces - - diff --git a/api_docs/cases.json b/api_docs/cases.json index ca02367539e60..8ac2fb86061bd 100644 --- a/api_docs/cases.json +++ b/api_docs/cases.json @@ -1,5 +1,5 @@ { - "id": "case", + "id": "cases", "client": { "classes": [], "functions": [], @@ -34,7 +34,7 @@ { "pluginId": "cases", "scope": "server", - "docId": "kibCasePluginApi", + "docId": "kibCasesPluginApi", "section": "def-server.CasesClient", "text": "CasesClient" } @@ -60,4 +60,4 @@ "misc": [], "objects": [] } -} +} \ No newline at end of file diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx new file mode 100644 index 0000000000000..36429f257d357 --- /dev/null +++ b/api_docs/cases.mdx @@ -0,0 +1,18 @@ +--- +id: kibCasesPluginApi +slug: /kibana-dev-docs/casesPluginApi +title: cases +image: https://source.unsplash.com/400x175/?github +summary: API docs for the cases plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] +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 casesObj from './cases.json'; + +## Server + +### Interfaces + + diff --git a/api_docs/core.json b/api_docs/core.json index 5446492e0e863..ba8f27d50d13c 100644 --- a/api_docs/core.json +++ b/api_docs/core.json @@ -6622,14 +6622,6 @@ "label": "asScoped", "signature": [ "(request?: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -6645,6 +6637,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined) => Pick<", { "pluginId": "core", @@ -6664,14 +6664,6 @@ "label": "request", "isRequired": false, "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -6687,6 +6679,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined" ], "description": [ @@ -16174,14 +16174,6 @@ }, "signature": [ "{ callAsInternalUser: LegacyAPICaller; asScoped: (request?: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -16197,6 +16189,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined) => Pick; }" ], "initialIsOpen": false @@ -16218,14 +16218,6 @@ }, "signature": [ "{ close: () => void; callAsInternalUser: LegacyAPICaller; asScoped: (request?: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -16241,6 +16233,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined) => Pick; }" ], "initialIsOpen": false @@ -16533,14 +16533,6 @@ "lineNumber": 192 }, "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -16555,6 +16547,14 @@ "docId": "kibCoreHttpPluginApi", "section": "def-server.LegacyRequest", "text": "LegacyRequest" + }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" } ], "initialIsOpen": false diff --git a/api_docs/core_http.json b/api_docs/core_http.json index c4a5bdd464c98..8053550cc0e80 100644 --- a/api_docs/core_http.json +++ b/api_docs/core_http.json @@ -2887,7 +2887,7 @@ "type": "Function", "label": "notHandled", "description": [ - "\nUser has no credentials.\nAllows user to access a resource when authRequired is 'optional' or 'try'\nRejects a request when authRequired: true" + "\nUser has no credentials.\nAllows user to access a resource when authRequired is 'optional'\nRejects a request when authRequired: true" ], "source": { "path": "src/core/server/http/lifecycle/auth.ts", @@ -4648,7 +4648,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 173 + "lineNumber": 171 } }, { @@ -4661,7 +4661,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 231 + "lineNumber": 229 }, "signature": [ "false | ", @@ -4685,7 +4685,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 236 + "lineNumber": 234 }, "signature": [ { @@ -4701,7 +4701,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 159 + "lineNumber": 157 }, "initialIsOpen": false }, @@ -4732,14 +4732,14 @@ "type": "CompoundType", "label": "authRequired", "description": [ - "\nDefines authentication mode for a route:\n- true. A user has to have valid credentials to access a resource\n- false. A user can access a resource without any credentials.\n- 'optional'. A user can access a resource if has valid credentials or no credentials at all.\n Can be useful when we grant access to a resource but want to identify a user if possible.\n- 'try'. A user can access a resource with valid, invalid or without any credentials.\n Users with valid credentials will be authenticated\n\nDefaults to `true` if an auth mechanism is registered." + "\nDefines authentication mode for a route:\n- true. A user has to have valid credentials to access a resource\n- false. A user can access a resource without any credentials.\n- 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid.\n Can be useful when we grant access to a resource but want to identify a user if possible.\n\nDefaults to `true` if an auth mechanism is registered." ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 118 + "lineNumber": 116 }, "signature": [ - "boolean | \"optional\" | \"try\" | undefined" + "boolean | \"optional\" | undefined" ] }, { @@ -4752,7 +4752,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 127 + "lineNumber": 125 }, "signature": [ "(Method extends \"get\" ? never : boolean) | undefined" @@ -4768,7 +4768,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 132 + "lineNumber": 130 }, "signature": [ "readonly string[] | undefined" @@ -4784,7 +4784,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 137 + "lineNumber": 135 }, "signature": [ "(Method extends ", @@ -4816,7 +4816,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 142 + "lineNumber": 140 }, "signature": [ "{ payload?: (Method extends ", diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 399850c519a5c..6dc7c105051f5 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -1435,7 +1435,15 @@ "section": "def-server.SearchUsage", "text": "SearchUsage" }, - " | undefined) => { next(response: ", + " | undefined, { isRestore }: ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.ISearchOptions", + "text": "ISearchOptions" + }, + ") => { next(response: ", { "pluginId": "data", "scope": "common", @@ -1459,7 +1467,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/collectors/usage.ts", - "lineNumber": 64 + "lineNumber": 83 } }, { @@ -1479,7 +1487,26 @@ "description": [], "source": { "path": "src/plugins/data/server/search/collectors/usage.ts", - "lineNumber": 64 + "lineNumber": 84 + } + }, + { + "type": "Object", + "label": "{ isRestore }", + "isRequired": true, + "signature": [ + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.ISearchOptions", + "text": "ISearchOptions" + } + ], + "description": [], + "source": { + "path": "src/plugins/data/server/search/collectors/usage.ts", + "lineNumber": 85 } } ], @@ -1487,7 +1514,7 @@ "returnComment": [], "source": { "path": "src/plugins/data/server/search/collectors/usage.ts", - "lineNumber": 64 + "lineNumber": 82 }, "initialIsOpen": false }, diff --git a/api_docs/lens.json b/api_docs/lens.json index 7e30ec6a15c3e..1c7581a8a1db6 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -107,7 +107,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 35 + "lineNumber": 43 }, "signature": [ { @@ -128,7 +128,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 36 + "lineNumber": 44 } }, { @@ -139,7 +139,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 37 + "lineNumber": 45 }, "signature": [ { @@ -155,7 +155,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 34 + "lineNumber": 42 }, "initialIsOpen": false }, diff --git a/api_docs/lists.json b/api_docs/lists.json index 077ea74fdff28..3e6a22c538504 100644 --- a/api_docs/lists.json +++ b/api_docs/lists.json @@ -3706,7 +3706,83 @@ }, "common": { "classes": [], - "functions": [], + "functions": [ + { + "id": "def-common.buildExceptionFilter", + "type": "Function", + "children": [ + { + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}", + "type": "Object", + "label": "{\n lists,\n excludeExceptions,\n chunkSize,\n}", + "tags": [], + "description": [], + "children": [ + { + "tags": [], + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}.lists", + "type": "Array", + "label": "lists", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 74 + }, + "signature": [ + "(({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }) | { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; })[]" + ] + }, + { + "tags": [], + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}.excludeExceptions", + "type": "boolean", + "label": "excludeExceptions", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 75 + } + }, + { + "tags": [], + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}.chunkSize", + "type": "number", + "label": "chunkSize", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 76 + } + } + ], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 73 + } + } + ], + "signature": [ + "({ lists, excludeExceptions, chunkSize, }: { lists: (({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }) | { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; })[]; excludeExceptions: boolean; chunkSize: number; }) => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + " | undefined" + ], + "description": [], + "label": "buildExceptionFilter", + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 69 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + } + ], "interfaces": [], "enums": [ { @@ -4413,6 +4489,22 @@ ], "initialIsOpen": false }, + { + "tags": [], + "id": "def-common.osType", + "type": "Object", + "label": "osType", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/schemas/common/schemas.ts", + "lineNumber": 317 + }, + "signature": [ + "KeyofC", + "<{ linux: null; macos: null; windows: null; }>" + ], + "initialIsOpen": false + }, { "tags": [], "id": "def-common.osTypeArray", diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 8e4a8efb7c24e..5d5f771548355 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -41,6 +41,9 @@ import listsObj from './lists.json'; ### Objects +### Functions + + ### Enums diff --git a/api_docs/security_solution.json b/api_docs/security_solution.json index 01fef476278b9..cbcd660749f2d 100644 --- a/api_docs/security_solution.json +++ b/api_docs/security_solution.json @@ -607,7 +607,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 336 + "lineNumber": 337 } }, { @@ -626,7 +626,7 @@ "description": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 336 + "lineNumber": 337 } } ], @@ -634,7 +634,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 336 + "lineNumber": 337 } }, { @@ -650,7 +650,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 397 + "lineNumber": 398 } } ], diff --git a/api_docs/vis_type_timeseries.json b/api_docs/vis_type_timeseries.json index 657e9a560060e..907ced500294a 100644 --- a/api_docs/vis_type_timeseries.json +++ b/api_docs/vis_type_timeseries.json @@ -30,7 +30,7 @@ "description": [], "source": { "path": "src/plugins/vis_type_timeseries/server/plugin.ts", - "lineNumber": 48 + "lineNumber": 53 }, "signature": [ "(requestContext: ", @@ -45,19 +45,11 @@ { "pluginId": "core", "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" }, - ", options: ", - { - "pluginId": "visTypeTimeseries", - "scope": "server", - "docId": "kibVisTypeTimeseriesPluginApi", - "section": "def-server.GetVisDataOptions", - "text": "GetVisDataOptions" - }, - ") => Promise<", + ", options: any) => Promise<", { "pluginId": "visTypeTimeseries", "scope": "common", @@ -71,7 +63,7 @@ ], "source": { "path": "src/plugins/vis_type_timeseries/server/plugin.ts", - "lineNumber": 47 + "lineNumber": 52 }, "lifecycle": "setup", "initialIsOpen": true diff --git a/docs/developer/index.asciidoc b/docs/developer/index.asciidoc index 9e349a38557f2..86d1d32e75e36 100644 --- a/docs/developer/index.asciidoc +++ b/docs/developer/index.asciidoc @@ -12,6 +12,7 @@ running in no time. If you have any problems, file an issue in the https://githu * <> * <> * <> +* <> -- @@ -29,3 +30,5 @@ include::advanced/index.asciidoc[] include::plugin-list.asciidoc[] +include::telemetry.asciidoc[] + diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 64433aa5946c2..c820c143bdcdf 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -165,7 +165,7 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s |{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects] -|The savedObjects plugin exposes utilities to manipulate saved objects on the client side. +|NOTE: This plugin is deprecated and will be removed in 8.0. See https://github.com/elastic/kibana/issues/46435 for more information. |{kib-repo}blob/{branch}/src/plugins/saved_objects_management/README.md[savedObjectsManagement] diff --git a/docs/developer/telemetry.asciidoc b/docs/developer/telemetry.asciidoc new file mode 100644 index 0000000000000..75f42a860624b --- /dev/null +++ b/docs/developer/telemetry.asciidoc @@ -0,0 +1,18 @@ +[[development-telemetry]] +== Development Telemetry + +To help us provide a good developer experience, we track some straightforward metrics when running certain tasks locally and ship them to a service that we run. To disable this functionality, specify `CI_STATS_DISABLED=true` in your environment. + +The operations we current report timing data for: + +* Total execution time of `yarn kbn bootstrap` + +Along with the execution time of each execution, we ship the following information about your machine to the service: + +* The `branch` property from the package.json file +* The value of the `data/uuid` file +* https://nodejs.org/docs/latest/api/os.html#os_os_platform[Operating system platform] +* https://nodejs.org/docs/latest/api/os.html#os_os_release[Operating system release] +* https://nodejs.org/docs/latest/api/os.html#os_os_cpus[Count, model, and speed of the CPUs] +* https://nodejs.org/docs/latest/api/os.html#os_os_arch[CPU architecture] +* https://nodejs.org/docs/latest/api/os.html#os_os_totalmem[Total memory] and https://nodejs.org/docs/latest/api/os.html#os_os_freemem[Free memory] \ No newline at end of file diff --git a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md index 0f0b070dbe87e..5f8b98ab2e894 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md @@ -17,6 +17,6 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | | [authenticated](./kibana-plugin-core-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | -| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired is 'optional' or 'try' Rejects a request when authRequired: true | +| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired is 'optional' Rejects a request when authRequired: true | | [redirected](./kibana-plugin-core-server.authtoolkit.redirected.md) | (headers: {
location: string;
} & ResponseHeaders) => AuthResult | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' | diff --git a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md index 7dc3b47e27e18..577faa6562558 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md +++ b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md @@ -4,7 +4,7 @@ ## AuthToolkit.notHandled property -User has no credentials. Allows user to access a resource when authRequired is 'optional' or 'try' Rejects a request when authRequired: true +User has no credentials. Allows user to access a resource when authRequired is 'optional' Rejects a request when authRequired: true Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md index 9f3822e5c206b..28f712316bc36 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md @@ -4,12 +4,12 @@ ## RouteConfigOptions.authRequired property -Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. - 'try'. A user can access a resource with valid, invalid or without any credentials. Users with valid credentials will be authenticated +Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. Can be useful when we grant access to a resource but want to identify a user if possible. Defaults to `true` if an auth mechanism is registered. Signature: ```typescript -authRequired?: boolean | 'optional' | 'try'; +authRequired?: boolean | 'optional'; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md index bd53570becf63..cf0fe32c14d1d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md @@ -16,7 +16,7 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | 'try' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. - 'try'. A user can access a resource with valid, invalid or without any credentials. Users with valid credentials will be authenticatedDefaults to true if an auth mechanism is registered. | +| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. Can be useful when we grant access to a resource but want to identify a user if possible.Defaults to true if an auth mechanism is registered. | | [body](./kibana-plugin-core-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-core-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | | [timeout](./kibana-plugin-core-server.routeconfigoptions.timeout.md) | {
payload?: Method extends 'get' | 'options' ? undefined : number;
idleSocket?: number;
} | Defines per-route timeouts. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 235f80f2fbdff..e0734bc017f4f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -35,7 +35,7 @@ | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | | | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | -| [searchUsageObserver(logger, usage)](./kibana-plugin-plugins-data-server.searchusageobserver.md) | Rxjs observer for easily doing tap(searchUsageObserver(logger, usage)) in an rxjs chain. | +| [searchUsageObserver(logger, usage, { isRestore })](./kibana-plugin-plugins-data-server.searchusageobserver.md) | Rxjs observer for easily doing tap(searchUsageObserver(logger, usage)) in an rxjs chain. | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | | [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusageobserver.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusageobserver.md index 5e03bb381527e..e9c7b33766e56 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusageobserver.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchusageobserver.md @@ -9,7 +9,7 @@ Rxjs observer for easily doing `tap(searchUsageObserver(logger, usage))` in an r Signature: ```typescript -export declare function searchUsageObserver(logger: Logger, usage?: SearchUsage): { +export declare function searchUsageObserver(logger: Logger, usage?: SearchUsage, { isRestore }?: ISearchOptions): { next(response: IEsSearchResponse): void; error(): void; }; @@ -21,6 +21,7 @@ export declare function searchUsageObserver(logger: Logger, usage?: SearchUsage) | --- | --- | --- | | logger | Logger | | | usage | SearchUsage | | +| { isRestore } | ISearchOptions | | Returns: diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 39835919a7fd4..606f113b2274f 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -12,11 +12,11 @@ This has three major benefits: [IMPORTANT] ============================================== - Task definitions for alerts and actions are stored in the index specified by <>. - The default is `.kibana_task_manager`. +Task definitions for alerts and actions are stored in the index specified by <>. The default is `.kibana_task_manager`. - You must have at least one replica of this index for production deployments. - If you lose this index, all scheduled alerts and actions are lost. +You must have at least one replica of this index for production deployments. + +If you lose this index, all scheduled alerts and actions are lost. ============================================== [float] diff --git a/packages/kbn-dev-utils/ci_stats_reporter/package.json b/packages/kbn-dev-utils/ci_stats_reporter/package.json new file mode 100644 index 0000000000000..a4f86a9239e31 --- /dev/null +++ b/packages/kbn-dev-utils/ci_stats_reporter/package.json @@ -0,0 +1,3 @@ +{ + "main": "../target/ci_stats_reporter/ci_stats_reporter" +} \ No newline at end of file diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_config.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_config.ts new file mode 100644 index 0000000000000..9af52ae8d2df0 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_config.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ToolingLog } from '../tooling_log'; + +export interface Config { + apiToken: string; + buildId: string; +} + +function validateConfig(log: ToolingLog, config: { [k in keyof Config]: unknown }) { + const validApiToken = typeof config.apiToken === 'string' && config.apiToken.length !== 0; + if (!validApiToken) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api token, stats will not be reported'); + return; + } + + const validId = typeof config.buildId === 'string' && config.buildId.length !== 0; + if (!validId) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid build id, stats will not be reported'); + return; + } + + return config as Config; +} + +export function parseConfig(log: ToolingLog) { + const configJson = process.env.KIBANA_CI_STATS_CONFIG; + if (!configJson) { + log.debug('KIBANA_CI_STATS_CONFIG environment variable not found, disabling CiStatsReporter'); + return; + } + + let config: unknown; + try { + config = JSON.parse(configJson); + } catch (_) { + // handled below + } + + if (typeof config === 'object' && config !== null) { + return validateConfig(log, config as { [k in keyof Config]: unknown }); + } + + log.warning('KIBANA_CI_STATS_CONFIG is invalid, stats will not be reported'); + return; +} diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index 93826cf3add80..7847cad0fd5e7 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -7,90 +7,178 @@ */ import { inspect } from 'util'; +import Os from 'os'; +import Fs from 'fs'; +import Path from 'path'; import Axios from 'axios'; import { ToolingLog } from '../tooling_log'; +import { parseConfig, Config } from './ci_stats_config'; -interface Config { - apiUrl: string; - apiToken: string; - buildId: string; -} +const BASE_URL = 'https://ci-stats.kibana.dev'; -export type CiStatsMetrics = Array<{ +export interface CiStatsMetric { group: string; id: string; value: number; limit?: number; limitConfigPath?: string; -}>; +} -function parseConfig(log: ToolingLog) { - const configJson = process.env.KIBANA_CI_STATS_CONFIG; - if (!configJson) { - log.debug('KIBANA_CI_STATS_CONFIG environment variable not found, disabling CiStatsReporter'); - return; - } +export interface CiStatsTimingMetadata { + [key: string]: string | string[] | number | boolean | undefined; +} +export interface CiStatsTiming { + group: string; + id: string; + ms: number; + meta?: CiStatsTimingMetadata; +} - let config: unknown; - try { - config = JSON.parse(configJson); - } catch (_) { - // handled below - } +export interface ReqOptions { + auth: boolean; + path: string; + body: any; + bodyDesc: string; +} - if (typeof config === 'object' && config !== null) { - return validateConfig(log, config as { [k in keyof Config]: unknown }); +export interface TimingsOptions { + /** list of timings to record */ + timings: CiStatsTiming[]; + /** master, 7.x, etc, automatically detected from package.json if not specified */ + upstreamBranch?: string; + /** value of data/uuid, automatically loaded if not specified */ + kibanaUuid?: string | null; +} +export class CiStatsReporter { + static fromEnv(log: ToolingLog) { + return new CiStatsReporter(parseConfig(log), log); } - log.warning('KIBANA_CI_STATS_CONFIG is invalid, stats will not be reported'); - return; -} + constructor(private config: Config | undefined, private log: ToolingLog) {} -function validateConfig(log: ToolingLog, config: { [k in keyof Config]: unknown }) { - const validApiUrl = typeof config.apiUrl === 'string' && config.apiUrl.length !== 0; - if (!validApiUrl) { - log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api url, stats will not be reported'); - return; + isEnabled() { + return process.env.CI_STATS_DISABLED !== 'true'; } - const validApiToken = typeof config.apiToken === 'string' && config.apiToken.length !== 0; - if (!validApiToken) { - log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api token, stats will not be reported'); - return; + hasBuildConfig() { + return this.isEnabled() && !!this.config?.apiToken && !!this.config?.buildId; } - const validId = typeof config.buildId === 'string' && config.buildId.length !== 0; - if (!validId) { - log.warning('KIBANA_CI_STATS_CONFIG is missing a valid build id, stats will not be reported'); - return; + /** + * Report timings data to the ci-stats service. If running in CI then the reporter + * will include the buildId in the report with the access token, otherwise the timings + * data will be recorded as anonymous timing data. + */ + async timings(options: TimingsOptions) { + if (!this.isEnabled()) { + return; + } + + const buildId = this.config?.buildId; + const timings = options.timings; + const upstreamBranch = options.upstreamBranch ?? this.getUpstreamBranch(); + const kibanaUuid = options.kibanaUuid === undefined ? this.getKibanaUuid() : options.kibanaUuid; + const defaultMetadata = { + osPlatform: Os.platform(), + osRelease: Os.release(), + osArch: Os.arch(), + cpuCount: Os.cpus()?.length, + cpuModel: Os.cpus()[0]?.model, + cpuSpeed: Os.cpus()[0]?.speed, + freeMem: Os.freemem(), + totalMem: Os.totalmem(), + kibanaUuid, + }; + + return await this.req({ + auth: !!buildId, + path: '/v1/timings', + body: { + buildId, + upstreamBranch, + timings, + defaultMetadata, + }, + bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings`, + }); } - return config as Config; -} + /** + * Report metrics data to the ci-stats service. If running outside of CI this method + * does nothing as metrics can only be reported when associated with a specific CI build. + */ + async metrics(metrics: CiStatsMetric[]) { + if (!this.hasBuildConfig()) { + return; + } -export class CiStatsReporter { - static fromEnv(log: ToolingLog) { - return new CiStatsReporter(parseConfig(log), log); - } + const buildId = this.config?.buildId; - constructor(private config: Config | undefined, private log: ToolingLog) {} + if (!buildId) { + throw new Error(`CiStatsReporter can't be authorized without a buildId`); + } - isEnabled() { - return !!this.config; + return await this.req({ + auth: true, + path: '/v1/metrics', + body: { + buildId, + metrics, + }, + bodyDesc: `metrics: ${metrics + .map(({ group, id, value }) => `[${group}/${id}=${value}]`) + .join(' ')}`, + }); } - async metrics(metrics: CiStatsMetrics) { - if (!this.config) { - return; + /** + * In order to allow this code to run before @kbn/utils is built, @kbn/pm will pass + * in the upstreamBranch when calling the timings() method. Outside of @kbn/pm + * we rely on @kbn/utils to find the package.json file. + */ + private getUpstreamBranch() { + // specify the module id in a way that will keep webpack from bundling extra code into @kbn/pm + const hideFromWebpack = ['@', 'kbn/utils']; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { kibanaPackageJson } = require(hideFromWebpack.join('')); + return kibanaPackageJson.branch; + } + + /** + * In order to allow this code to run before @kbn/utils is built, @kbn/pm will pass + * in the kibanaUuid when calling the timings() method. Outside of @kbn/pm + * we rely on @kbn/utils to find the repo root. + */ + private getKibanaUuid() { + // specify the module id in a way that will keep webpack from bundling extra code into @kbn/pm + const hideFromWebpack = ['@', 'kbn/utils']; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { REPO_ROOT } = require(hideFromWebpack.join('')); + try { + return Fs.readFileSync(Path.resolve(REPO_ROOT, 'data/uuid'), 'utf-8').trim(); + } catch (error) { + if (error.code === 'ENOENT') { + return undefined; + } + + throw error; } + } + private async req({ auth, body, bodyDesc, path }: ReqOptions) { let attempt = 0; const maxAttempts = 5; - const bodySummary = metrics - .map(({ group, id, value }) => `[${group}/${id}=${value}]`) - .join(' '); + + let headers; + if (auth && this.config) { + headers = { + Authorization: `token ${this.config.apiToken}`, + }; + } else if (auth) { + throw new Error('this.req() shouldnt be called with auth=true if this.config is defined'); + } while (true) { attempt += 1; @@ -98,15 +186,10 @@ export class CiStatsReporter { try { await Axios.request({ method: 'POST', - url: '/v1/metrics', - baseURL: this.config.apiUrl, - headers: { - Authorization: `token ${this.config.apiToken}`, - }, - data: { - buildId: this.config.buildId, - metrics, - }, + url: path, + baseURL: BASE_URL, + headers, + data: body, }); return true; @@ -116,19 +199,19 @@ export class CiStatsReporter { throw error; } - if (error?.response && error.response.status !== 502) { + if (error?.response && error.response.status < 502) { // error response from service was received so warn the user and move on this.log.warning( - `error recording metric [status=${error.response.status}] [resp=${inspect( + `error reporting ${bodyDesc} [status=${error.response.status}] [resp=${inspect( error.response.data - )}] ${bodySummary}` + )}]` ); return; } if (attempt === maxAttempts) { this.log.warning( - `failed to reach kibana-ci-stats service too many times, unable to record metric ${bodySummary}` + `unable to report ${bodyDesc}, failed to reach ci-stats service too many times` ); return; } @@ -139,7 +222,7 @@ export class CiStatsReporter { : 'no response'; this.log.warning( - `failed to reach kibana-ci-stats service [reason=${reason}], retrying in ${attempt} seconds` + `failed to reach ci-stats service [reason=${reason}], retrying in ${attempt} seconds` ); await new Promise((resolve) => setTimeout(resolve, attempt * 1000)); diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts index 077fe38ddc7f6..4479e0acc097e 100644 --- a/packages/kbn-optimizer/src/limits.ts +++ b/packages/kbn-optimizer/src/limits.ts @@ -11,7 +11,7 @@ import Path from 'path'; import dedent from 'dedent'; import Yaml from 'js-yaml'; -import { createFailError, ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; +import { createFailError, ToolingLog, CiStatsMetric } from '@kbn/dev-utils'; import { OptimizerConfig, Limits } from './optimizer'; @@ -86,7 +86,7 @@ export function updateBundleLimits({ limitsPath, }: UpdateBundleLimitsOptions) { const limits = readLimits(limitsPath); - const metrics: CiStatsMetrics = config.bundles + const metrics: CiStatsMetric[] = config.bundles .map((bundle) => JSON.parse(Fs.readFileSync(Path.resolve(bundle.outputDir, 'metrics.json'), 'utf-8')) ) diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts index 909a97a3e11c7..92875d3f69e46 100644 --- a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts +++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts @@ -10,7 +10,7 @@ import Path from 'path'; import webpack from 'webpack'; import { RawSource } from 'webpack-sources'; -import { CiStatsMetrics } from '@kbn/dev-utils'; +import { CiStatsMetric } from '@kbn/dev-utils'; import { Bundle } from '../common'; @@ -68,7 +68,7 @@ export class BundleMetricsPlugin { throw new Error(`moduleCount wasn't populated by PopulateBundleCachePlugin`); } - const bundleMetrics: CiStatsMetrics = [ + const bundleMetrics: CiStatsMetric[] = [ { group: `@kbn/optimizer bundle module count`, id: bundle.id, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 6c032a4e855c4..206c5c62d2472 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(520); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(563); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildBazelProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); @@ -108,7 +108,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(519); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(562); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -8895,6 +8895,10 @@ __webpack_require__.r(__webpack_exports__); const BootstrapCommand = { description: 'Install dependencies and crosslink projects', name: 'bootstrap', + reportTiming: { + group: 'bootstrap', + id: 'overall time' + }, async run(projects, projectGraph, { options, @@ -59490,11 +59494,13 @@ function waitUntilWatchIsReady(stream, opts = {}) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); -/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(249); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(371); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(515); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(515); +/* harmony import */ var _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(249); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(246); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(371); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(558); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59513,10 +59519,14 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope + async function runCommand(command, config) { + const runStartTime = Date.now(); + let kbn; + try { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].debug(`Running [${command.name}] command from [${config.rootPath}]`); - const kbn = await _utils_kibana__WEBPACK_IMPORTED_MODULE_4__["Kibana"].loadFrom(config.rootPath); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].debug(`Running [${command.name}] command from [${config.rootPath}]`); + kbn = await _utils_kibana__WEBPACK_IMPORTED_MODULE_5__["Kibana"].loadFrom(config.rootPath); const projects = kbn.getFilteredProjects({ skipKibanaPlugins: Boolean(config.options['skip-kibana-plugins']), ossOnly: Boolean(config.options.oss), @@ -59525,31 +59535,66 @@ async function runCommand(command, config) { }); if (projects.size === 0) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].error(`There are no projects found. Double check project name(s) in '-i/--include' and '-e/--exclude' filters.`); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].error(`There are no projects found. Double check project name(s) in '-i/--include' and '-e/--exclude' filters.`); return process.exit(1); } - const projectGraph = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_2__["buildProjectGraph"])(projects); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].debug(`Found ${projects.size.toString()} projects`); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].debug(Object(_utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__["renderProjectsTree"])(config.rootPath, projects)); + const projectGraph = Object(_utils_projects__WEBPACK_IMPORTED_MODULE_3__["buildProjectGraph"])(projects); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].debug(`Found ${projects.size.toString()} projects`); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].debug(Object(_utils_projects_tree__WEBPACK_IMPORTED_MODULE_4__["renderProjectsTree"])(config.rootPath, projects)); await command.run(projects, projectGraph, _objectSpread(_objectSpread({}, config), {}, { kbn })); + + if (command.reportTiming) { + const reporter = _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__["CiStatsReporter"].fromEnv(_utils_log__WEBPACK_IMPORTED_MODULE_2__["log"]); + await reporter.timings({ + upstreamBranch: kbn.kibanaProject.json.branch, + // prevent loading @kbn/utils by passing null + kibanaUuid: kbn.getUuid() || null, + timings: [{ + group: command.reportTiming.group, + id: command.reportTiming.id, + ms: Date.now() - runStartTime, + meta: { + success: true + } + }] + }); + } } catch (error) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].error(`[${command.name}] failed:`); + if (command.reportTiming) { + // if we don't have a kbn object then things are too broken to report on + if (kbn) { + const reporter = _kbn_dev_utils_ci_stats_reporter__WEBPACK_IMPORTED_MODULE_0__["CiStatsReporter"].fromEnv(_utils_log__WEBPACK_IMPORTED_MODULE_2__["log"]); + await reporter.timings({ + upstreamBranch: kbn.kibanaProject.json.branch, + timings: [{ + group: command.reportTiming.group, + id: command.reportTiming.id, + ms: Date.now() - runStartTime, + meta: { + success: false + } + }] + }); + } + } - if (error instanceof _utils_errors__WEBPACK_IMPORTED_MODULE_0__["CliError"]) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].error(error.message); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].error(`[${command.name}] failed:`); + + if (error instanceof _utils_errors__WEBPACK_IMPORTED_MODULE_1__["CliError"]) { + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].error(error.message); const metaOutput = Object.entries(error.meta).map(([key, value]) => `${key}: ${value}`).join('\n'); if (metaOutput) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].info('Additional debugging info:\n'); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].indent(2); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].info(metaOutput); - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].indent(-2); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].info('Additional debugging info:\n'); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].indent(2); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].info(metaOutput); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].indent(-2); } } else { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].error(error); + _utils_log__WEBPACK_IMPORTED_MODULE_2__["log"].error(error); } process.exit(1); @@ -59566,25 +59611,9 @@ function toArray(value) { /***/ }), /* 515 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(516); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(239); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(366); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(519); -function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } - -function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one @@ -59593,834 +59622,4522 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CiStatsReporter = void 0; +const tslib_1 = __webpack_require__(7); +const util_1 = __webpack_require__(112); +const os_1 = tslib_1.__importDefault(__webpack_require__(121)); +const fs_1 = tslib_1.__importDefault(__webpack_require__(134)); +const path_1 = tslib_1.__importDefault(__webpack_require__(4)); +const axios_1 = tslib_1.__importDefault(__webpack_require__(516)); +const ci_stats_config_1 = __webpack_require__(556); +const BASE_URL = 'https://ci-stats.kibana.dev'; +class CiStatsReporter { + constructor(config, log) { + this.config = config; + this.log = log; + } + static fromEnv(log) { + return new CiStatsReporter(ci_stats_config_1.parseConfig(log), log); + } + isEnabled() { + return process.env.CI_STATS_DISABLED !== 'true'; + } + hasBuildConfig() { + var _a, _b; + return this.isEnabled() && !!((_a = this.config) === null || _a === void 0 ? void 0 : _a.apiToken) && !!((_b = this.config) === null || _b === void 0 ? void 0 : _b.buildId); + } + /** + * Report timings data to the ci-stats service. If running in CI then the reporter + * will include the buildId in the report with the access token, otherwise the timings + * data will be recorded as anonymous timing data. + */ + async timings(options) { + var _a, _b, _c, _d, _e; + if (!this.isEnabled()) { + return; + } + const buildId = (_a = this.config) === null || _a === void 0 ? void 0 : _a.buildId; + const timings = options.timings; + const upstreamBranch = (_b = options.upstreamBranch) !== null && _b !== void 0 ? _b : this.getUpstreamBranch(); + const kibanaUuid = options.kibanaUuid === undefined ? this.getKibanaUuid() : options.kibanaUuid; + const defaultMetadata = { + osPlatform: os_1.default.platform(), + osRelease: os_1.default.release(), + osArch: os_1.default.arch(), + cpuCount: (_c = os_1.default.cpus()) === null || _c === void 0 ? void 0 : _c.length, + cpuModel: (_d = os_1.default.cpus()[0]) === null || _d === void 0 ? void 0 : _d.model, + cpuSpeed: (_e = os_1.default.cpus()[0]) === null || _e === void 0 ? void 0 : _e.speed, + freeMem: os_1.default.freemem(), + totalMem: os_1.default.totalmem(), + kibanaUuid, + }; + return await this.req({ + auth: !!buildId, + path: '/v1/timings', + body: { + buildId, + upstreamBranch, + timings, + defaultMetadata, + }, + bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings`, + }); + } + /** + * Report metrics data to the ci-stats service. If running outside of CI this method + * does nothing as metrics can only be reported when associated with a specific CI build. + */ + async metrics(metrics) { + var _a; + if (!this.hasBuildConfig()) { + return; + } + const buildId = (_a = this.config) === null || _a === void 0 ? void 0 : _a.buildId; + if (!buildId) { + throw new Error(`CiStatsReporter can't be authorized without a buildId`); + } + return await this.req({ + auth: true, + path: '/v1/metrics', + body: { + buildId, + metrics, + }, + bodyDesc: `metrics: ${metrics + .map(({ group, id, value }) => `[${group}/${id}=${value}]`) + .join(' ')}`, + }); + } + /** + * In order to allow this code to run before @kbn/utils is built, @kbn/pm will pass + * in the upstreamBranch when calling the timings() method. Outside of @kbn/pm + * we rely on @kbn/utils to find the package.json file. + */ + getUpstreamBranch() { + // specify the module id in a way that will keep webpack from bundling extra code into @kbn/pm + const hideFromWebpack = ['@', 'kbn/utils']; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { kibanaPackageJson } = __webpack_require__(557)(hideFromWebpack.join('')); + return kibanaPackageJson.branch; + } + /** + * In order to allow this code to run before @kbn/utils is built, @kbn/pm will pass + * in the kibanaUuid when calling the timings() method. Outside of @kbn/pm + * we rely on @kbn/utils to find the repo root. + */ + getKibanaUuid() { + // specify the module id in a way that will keep webpack from bundling extra code into @kbn/pm + const hideFromWebpack = ['@', 'kbn/utils']; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { REPO_ROOT } = __webpack_require__(557)(hideFromWebpack.join('')); + try { + return fs_1.default.readFileSync(path_1.default.resolve(REPO_ROOT, 'data/uuid'), 'utf-8').trim(); + } + catch (error) { + if (error.code === 'ENOENT') { + return undefined; + } + throw error; + } + } + async req({ auth, body, bodyDesc, path }) { + var _a; + let attempt = 0; + const maxAttempts = 5; + let headers; + if (auth && this.config) { + headers = { + Authorization: `token ${this.config.apiToken}`, + }; + } + else if (auth) { + throw new Error('this.req() shouldnt be called with auth=true if this.config is defined'); + } + while (true) { + attempt += 1; + try { + await axios_1.default.request({ + method: 'POST', + url: path, + baseURL: BASE_URL, + headers, + data: body, + }); + return true; + } + catch (error) { + if (!(error === null || error === void 0 ? void 0 : error.request)) { + // not an axios error, must be a usage error that we should notify user about + throw error; + } + if ((error === null || error === void 0 ? void 0 : error.response) && error.response.status < 502) { + // error response from service was received so warn the user and move on + this.log.warning(`error reporting ${bodyDesc} [status=${error.response.status}] [resp=${util_1.inspect(error.response.data)}]`); + return; + } + if (attempt === maxAttempts) { + this.log.warning(`unable to report ${bodyDesc}, failed to reach ci-stats service too many times`); + return; + } + // we failed to reach the backend and we have remaining attempts, lets retry after a short delay + const reason = ((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) ? `${error.response.status} response` + : 'no response'; + this.log.warning(`failed to reach ci-stats service [reason=${reason}], retrying in ${attempt} seconds`); + await new Promise((resolve) => setTimeout(resolve, attempt * 1000)); + } + } + } +} +exports.CiStatsReporter = CiStatsReporter; + + +/***/ }), +/* 516 */ +/***/ (function(module, exports, __webpack_require__) { +module.exports = __webpack_require__(517); +/***/ }), +/* 517 */ +/***/ (function(module, exports, __webpack_require__) { +"use strict"; +var utils = __webpack_require__(518); +var bind = __webpack_require__(519); +var Axios = __webpack_require__(520); +var mergeConfig = __webpack_require__(551); +var defaults = __webpack_require__(526); /** - * Helper class for dealing with a set of projects as children of - * the Kibana project. The kbn/pm is currently implemented to be - * more generic, where everything is an operation of generic projects, - * but that leads to exceptions where we need the kibana project and - * do things like `project.get('kibana')!`. + * Create an instance of Axios * - * Using this helper we can restructre the generic list of projects - * as a Kibana object which encapulates all the projects in the - * workspace and knows about the root Kibana project. + * @param {Object} defaultConfig The default config for the instance + * @return {Axios} A new instance of Axios */ +function createInstance(defaultConfig) { + var context = new Axios(defaultConfig); + var instance = bind(Axios.prototype.request, context); -class Kibana { - static async loadFrom(rootPath) { - return new Kibana(await Object(_projects__WEBPACK_IMPORTED_MODULE_4__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"])({ - rootPath - }))); - } + // Copy axios.prototype to instance + utils.extend(instance, Axios.prototype, context); - constructor(allWorkspaceProjects) { - this.allWorkspaceProjects = allWorkspaceProjects; + // Copy context to instance + utils.extend(instance, context); - _defineProperty(this, "kibanaProject", void 0); + return instance; +} - const kibanaProject = allWorkspaceProjects.get('kibana'); +// Create the default instance to be exported +var axios = createInstance(defaults); - if (!kibanaProject) { - throw new TypeError('Unable to create Kibana object without all projects, including the Kibana project.'); - } +// Expose Axios class to allow class inheritance +axios.Axios = Axios; - this.kibanaProject = kibanaProject; - } - /** make an absolute path by resolving subPath relative to the kibana repo */ +// Factory for creating new instances +axios.create = function create(instanceConfig) { + return createInstance(mergeConfig(axios.defaults, instanceConfig)); +}; +// Expose Cancel & CancelToken +axios.Cancel = __webpack_require__(552); +axios.CancelToken = __webpack_require__(553); +axios.isCancel = __webpack_require__(525); - getAbsolute(...subPath) { - return path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(this.kibanaProject.path, ...subPath); - } - /** convert an absolute path to a relative path, relative to the kibana repo */ +// Expose all/spread +axios.all = function all(promises) { + return Promise.all(promises); +}; +axios.spread = __webpack_require__(554); +// Expose isAxiosError +axios.isAxiosError = __webpack_require__(555); - getRelative(absolute) { - return path__WEBPACK_IMPORTED_MODULE_0___default.a.relative(this.kibanaProject.path, absolute); - } - /** get a copy of the map of all projects in the kibana workspace */ +module.exports = axios; +// Allow use of default import syntax in TypeScript +module.exports.default = axios; - getAllProjects() { - return new Map(this.allWorkspaceProjects); - } - /** determine if a project with the given name exists */ +/***/ }), +/* 518 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; - hasProject(name) { - return this.allWorkspaceProjects.has(name); - } - /** get a specific project, throws if the name is not known (use hasProject() first) */ +var bind = __webpack_require__(519); - getProject(name) { - const project = this.allWorkspaceProjects.get(name); +/*global toString:true*/ - if (!project) { - throw new Error(`No package with name "${name}" in the workspace`); - } +// utils is a library of generic helper functions non-specific to axios - return project; +var toString = Object.prototype.toString; + +/** + * Determine if a value is an Array + * + * @param {Object} val The value to test + * @returns {boolean} True if value is an Array, otherwise false + */ +function isArray(val) { + return toString.call(val) === '[object Array]'; +} + +/** + * Determine if a value is undefined + * + * @param {Object} val The value to test + * @returns {boolean} True if the value is undefined, otherwise false + */ +function isUndefined(val) { + return typeof val === 'undefined'; +} + +/** + * Determine if a value is a Buffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Buffer, otherwise false + */ +function isBuffer(val) { + return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) + && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val); +} + +/** + * Determine if a value is an ArrayBuffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is an ArrayBuffer, otherwise false + */ +function isArrayBuffer(val) { + return toString.call(val) === '[object ArrayBuffer]'; +} + +/** + * Determine if a value is a FormData + * + * @param {Object} val The value to test + * @returns {boolean} True if value is an FormData, otherwise false + */ +function isFormData(val) { + return (typeof FormData !== 'undefined') && (val instanceof FormData); +} + +/** + * Determine if a value is a view on an ArrayBuffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false + */ +function isArrayBufferView(val) { + var result; + if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) { + result = ArrayBuffer.isView(val); + } else { + result = (val) && (val.buffer) && (val.buffer instanceof ArrayBuffer); } - /** get a project and all of the projects it depends on in a ProjectMap */ + return result; +} +/** + * Determine if a value is a String + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a String, otherwise false + */ +function isString(val) { + return typeof val === 'string'; +} - getProjectAndDeps(name) { - const project = this.getProject(name); - return Object(_projects__WEBPACK_IMPORTED_MODULE_4__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); +/** + * Determine if a value is a Number + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Number, otherwise false + */ +function isNumber(val) { + return typeof val === 'number'; +} + +/** + * Determine if a value is an Object + * + * @param {Object} val The value to test + * @returns {boolean} True if value is an Object, otherwise false + */ +function isObject(val) { + return val !== null && typeof val === 'object'; +} + +/** + * Determine if a value is a plain Object + * + * @param {Object} val The value to test + * @return {boolean} True if value is a plain Object, otherwise false + */ +function isPlainObject(val) { + if (toString.call(val) !== '[object Object]') { + return false; } - /** filter the projects to just those matching certain paths/include/exclude tags */ + var prototype = Object.getPrototypeOf(val); + return prototype === null || prototype === Object.prototype; +} - getFilteredProjects(options) { - const allProjects = this.getAllProjects(); - const filteredProjects = new Map(); - const pkgJsonPaths = Array.from(allProjects.values()).map(p => p.packageJsonLocation); - const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"])(_objectSpread(_objectSpread({}, options), {}, { - rootPath: this.kibanaProject.path - })).map(g => path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(g, 'package.json')); - const matchingPkgJsonPaths = multimatch__WEBPACK_IMPORTED_MODULE_1___default()(pkgJsonPaths, filteredPkgJsonGlobs); +/** + * Determine if a value is a Date + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Date, otherwise false + */ +function isDate(val) { + return toString.call(val) === '[object Date]'; +} - for (const project of allProjects.values()) { - const pathMatches = matchingPkgJsonPaths.includes(project.packageJsonLocation); - const notExcluded = !options.exclude.includes(project.name); - const isIncluded = !options.include.length || options.include.includes(project.name); +/** + * Determine if a value is a File + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a File, otherwise false + */ +function isFile(val) { + return toString.call(val) === '[object File]'; +} - if (pathMatches && notExcluded && isIncluded) { - filteredProjects.set(project.name, project); - } - } +/** + * Determine if a value is a Blob + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Blob, otherwise false + */ +function isBlob(val) { + return toString.call(val) === '[object Blob]'; +} - return filteredProjects; +/** + * Determine if a value is a Function + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Function, otherwise false + */ +function isFunction(val) { + return toString.call(val) === '[object Function]'; +} + +/** + * Determine if a value is a Stream + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Stream, otherwise false + */ +function isStream(val) { + return isObject(val) && isFunction(val.pipe); +} + +/** + * Determine if a value is a URLSearchParams object + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a URLSearchParams object, otherwise false + */ +function isURLSearchParams(val) { + return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams; +} + +/** + * Trim excess whitespace off the beginning and end of a string + * + * @param {String} str The String to trim + * @returns {String} The String freed of excess whitespace + */ +function trim(str) { + return str.replace(/^\s*/, '').replace(/\s*$/, ''); +} + +/** + * Determine if we're running in a standard browser environment + * + * This allows axios to run in a web worker, and react-native. + * Both environments support XMLHttpRequest, but not fully standard globals. + * + * web workers: + * typeof window -> undefined + * typeof document -> undefined + * + * react-native: + * navigator.product -> 'ReactNative' + * nativescript + * navigator.product -> 'NativeScript' or 'NS' + */ +function isStandardBrowserEnv() { + if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' || + navigator.product === 'NativeScript' || + navigator.product === 'NS')) { + return false; } + return ( + typeof window !== 'undefined' && + typeof document !== 'undefined' + ); +} - isPartOfRepo(project) { - return project.path === this.kibanaProject.path || is_path_inside__WEBPACK_IMPORTED_MODULE_2___default()(project.path, this.kibanaProject.path); +/** + * Iterate over an Array or an Object invoking a function for each item. + * + * If `obj` is an Array callback will be called passing + * the value, index, and complete array for each item. + * + * If 'obj' is an Object callback will be called passing + * the value, key, and complete object for each property. + * + * @param {Object|Array} obj The object to iterate + * @param {Function} fn The callback to invoke for each item + */ +function forEach(obj, fn) { + // Don't bother if no value provided + if (obj === null || typeof obj === 'undefined') { + return; } - isOutsideRepo(project) { - return !this.isPartOfRepo(project); + // Force an array if not already something iterable + if (typeof obj !== 'object') { + /*eslint no-param-reassign:0*/ + obj = [obj]; } - resolveAllProductionDependencies(yarnLock, log) { - const kibanaDeps = Object(_yarn_lock__WEBPACK_IMPORTED_MODULE_3__["resolveDepsForProject"])({ - project: this.kibanaProject, - yarnLock, - kbn: this, - includeDependentProject: true, - productionDepsOnly: true, - log - }); - const xpackDeps = Object(_yarn_lock__WEBPACK_IMPORTED_MODULE_3__["resolveDepsForProject"])({ - project: this.getProject('x-pack'), - yarnLock, - kbn: this, - includeDependentProject: true, - productionDepsOnly: true, - log - }); - return new Map([...kibanaDeps.entries(), ...xpackDeps.entries()]); + if (isArray(obj)) { + // Iterate over array values + for (var i = 0, l = obj.length; i < l; i++) { + fn.call(null, obj[i], i, obj); + } + } else { + // Iterate over object keys + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + fn.call(null, obj[key], key, obj); + } + } + } +} + +/** + * Accepts varargs expecting each argument to be an object, then + * immutably merges the properties of each object and returns result. + * + * When multiple objects contain the same key the later object in + * the arguments list will take precedence. + * + * Example: + * + * ```js + * var result = merge({foo: 123}, {foo: 456}); + * console.log(result.foo); // outputs 456 + * ``` + * + * @param {Object} obj1 Object to merge + * @returns {Object} Result of all merge properties + */ +function merge(/* obj1, obj2, obj3, ... */) { + var result = {}; + function assignValue(val, key) { + if (isPlainObject(result[key]) && isPlainObject(val)) { + result[key] = merge(result[key], val); + } else if (isPlainObject(val)) { + result[key] = merge({}, val); + } else if (isArray(val)) { + result[key] = val.slice(); + } else { + result[key] = val; + } } + for (var i = 0, l = arguments.length; i < l; i++) { + forEach(arguments[i], assignValue); + } + return result; } -/***/ }), -/* 516 */ -/***/ (function(module, exports, __webpack_require__) { +/** + * Extends object a by mutably adding to it the properties of object b. + * + * @param {Object} a The object to be extended + * @param {Object} b The object to copy properties from + * @param {Object} thisArg The object to bind function to + * @return {Object} The resulting value of object a + */ +function extend(a, b, thisArg) { + forEach(b, function assignValue(val, key) { + if (thisArg && typeof val === 'function') { + a[key] = bind(val, thisArg); + } else { + a[key] = val; + } + }); + return a; +} -"use strict"; +/** + * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) + * + * @param {string} content with BOM + * @return {string} content value without BOM + */ +function stripBOM(content) { + if (content.charCodeAt(0) === 0xFEFF) { + content = content.slice(1); + } + return content; +} -const minimatch = __webpack_require__(150); -const arrayUnion = __webpack_require__(145); -const arrayDiffer = __webpack_require__(517); -const arrify = __webpack_require__(518); +module.exports = { + isArray: isArray, + isArrayBuffer: isArrayBuffer, + isBuffer: isBuffer, + isFormData: isFormData, + isArrayBufferView: isArrayBufferView, + isString: isString, + isNumber: isNumber, + isObject: isObject, + isPlainObject: isPlainObject, + isUndefined: isUndefined, + isDate: isDate, + isFile: isFile, + isBlob: isBlob, + isFunction: isFunction, + isStream: isStream, + isURLSearchParams: isURLSearchParams, + isStandardBrowserEnv: isStandardBrowserEnv, + forEach: forEach, + merge: merge, + extend: extend, + trim: trim, + stripBOM: stripBOM +}; -module.exports = (list, patterns, options = {}) => { - list = arrify(list); - patterns = arrify(patterns); - if (list.length === 0 || patterns.length === 0) { - return []; - } +/***/ }), +/* 519 */ +/***/ (function(module, exports, __webpack_require__) { - return patterns.reduce((result, pattern) => { - let process = arrayUnion; +"use strict"; - if (pattern[0] === '!') { - pattern = pattern.slice(1); - process = arrayDiffer; - } - return process(result, minimatch.match(list, pattern, options)); - }, []); +module.exports = function bind(fn, thisArg) { + return function wrap() { + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + return fn.apply(thisArg, args); + }; }; /***/ }), -/* 517 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const arrayDiffer = (array, ...values) => { - const rest = new Set([].concat(...values)); - return array.filter(element => !rest.has(element)); -}; +var utils = __webpack_require__(518); +var buildURL = __webpack_require__(521); +var InterceptorManager = __webpack_require__(522); +var dispatchRequest = __webpack_require__(523); +var mergeConfig = __webpack_require__(551); -module.exports = arrayDiffer; +/** + * Create a new instance of Axios + * + * @param {Object} instanceConfig The default config for the instance + */ +function Axios(instanceConfig) { + this.defaults = instanceConfig; + this.interceptors = { + request: new InterceptorManager(), + response: new InterceptorManager() + }; +} +/** + * Dispatch a request + * + * @param {Object} config The config specific for this request (merged with this.defaults) + */ +Axios.prototype.request = function request(config) { + /*eslint no-param-reassign:0*/ + // Allow for axios('example/url'[, config]) a la fetch API + if (typeof config === 'string') { + config = arguments[1] || {}; + config.url = arguments[0]; + } else { + config = config || {}; + } -/***/ }), -/* 518 */ -/***/ (function(module, exports, __webpack_require__) { + config = mergeConfig(this.defaults, config); -"use strict"; + // Set config.method + if (config.method) { + config.method = config.method.toLowerCase(); + } else if (this.defaults.method) { + config.method = this.defaults.method.toLowerCase(); + } else { + config.method = 'get'; + } + // Hook up interceptors middleware + var chain = [dispatchRequest, undefined]; + var promise = Promise.resolve(config); -const arrify = value => { - if (value === null || value === undefined) { - return []; - } + this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { + chain.unshift(interceptor.fulfilled, interceptor.rejected); + }); - if (Array.isArray(value)) { - return value; - } + this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { + chain.push(interceptor.fulfilled, interceptor.rejected); + }); - if (typeof value === 'string') { - return [value]; - } + while (chain.length) { + promise = promise.then(chain.shift(), chain.shift()); + } - if (typeof value[Symbol.iterator] === 'function') { - return [...value]; - } + return promise; +}; - return [value]; +Axios.prototype.getUri = function getUri(config) { + config = mergeConfig(this.defaults, config); + return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, ''); }; -module.exports = arrify; +// Provide aliases for supported request methods +utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { + /*eslint func-names:0*/ + Axios.prototype[method] = function(url, config) { + return this.request(mergeConfig(config || {}, { + method: method, + url: url, + data: (config || {}).data + })); + }; +}); + +utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { + /*eslint func-names:0*/ + Axios.prototype[method] = function(url, data, config) { + return this.request(mergeConfig(config || {}, { + method: method, + url: url, + data: data + })); + }; +}); + +module.exports = Axios; /***/ }), -/* 519 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 521 */ +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return getProjectPaths; }); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_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. - */ +var utils = __webpack_require__(518); + +function encode(val) { + return encodeURIComponent(val). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, '+'). + replace(/%5B/gi, '['). + replace(/%5D/gi, ']'); +} + /** - * Returns all the paths where plugins are located + * Build a URL by appending params to the end + * + * @param {string} url The base of the url (e.g., http://www.google.com) + * @param {object} [params] The params to be appended + * @returns {string} The formatted url */ -function getProjectPaths({ - rootPath, - ossOnly, - skipKibanaPlugins -}) { - const projectPaths = [rootPath, Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'packages/*')]; // This is needed in order to install the dependencies for the declared - // plugin functional used in the selenium functional tests. - // As we are now using the webpack dll for the client vendors dependencies - // when we run the plugin functional tests against the distributable - // dependencies used by such plugins like @eui, react and react-dom can't - // be loaded from the dll as the context is different from the one declared - // into the webpack dll reference plugin. - // In anyway, have a plugin declaring their own dependencies is the - // correct and the expect behavior. +module.exports = function buildURL(url, params, paramsSerializer) { + /*eslint no-param-reassign:0*/ + if (!params) { + return url; + } - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*')); + var serializedParams; + if (paramsSerializer) { + serializedParams = paramsSerializer(params); + } else if (utils.isURLSearchParams(params)) { + serializedParams = params.toString(); + } else { + var parts = []; - if (!ossOnly) { - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/plugins/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/legacy/plugins/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/test/functional_with_es_ssl/fixtures/plugins/*')); + utils.forEach(params, function serialize(val, key) { + if (val === null || typeof val === 'undefined') { + return; + } + + if (utils.isArray(val)) { + key = key + '[]'; + } else { + val = [val]; + } + + utils.forEach(val, function parseValue(v) { + if (utils.isDate(v)) { + v = v.toISOString(); + } else if (utils.isObject(v)) { + v = JSON.stringify(v); + } + parts.push(encode(key) + '=' + encode(v)); + }); + }); + + serializedParams = parts.join('&'); } - if (!skipKibanaPlugins) { - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, '../kibana-extra/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, '../kibana-extra/*/packages/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, '../kibana-extra/*/plugins/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'plugins/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'plugins/*/packages/*')); - projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'plugins/*/plugins/*')); + if (serializedParams) { + var hashmarkIndex = url.indexOf('#'); + if (hashmarkIndex !== -1) { + url = url.slice(0, hashmarkIndex); + } + + url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; } - return projectPaths; -} + return url; +}; + /***/ }), -/* 520 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 522 */ +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(521); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(746); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); -/* - * 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. +var utils = __webpack_require__(518); + +function InterceptorManager() { + this.handlers = []; +} + +/** + * Add a new interceptor to the stack + * + * @param {Function} fulfilled The function to handle `then` for a `Promise` + * @param {Function} rejected The function to handle `reject` for a `Promise` + * + * @return {Number} An ID used to remove interceptor later + */ +InterceptorManager.prototype.use = function use(fulfilled, rejected) { + this.handlers.push({ + fulfilled: fulfilled, + rejected: rejected + }); + return this.handlers.length - 1; +}; + +/** + * Remove an interceptor from the stack + * + * @param {Number} id The ID that was returned by `use` + */ +InterceptorManager.prototype.eject = function eject(id) { + if (this.handlers[id]) { + this.handlers[id] = null; + } +}; + +/** + * Iterate over all the registered interceptors + * + * This method is particularly useful for skipping over any + * interceptors that may have become `null` calling `eject`. + * + * @param {Function} fn The function to call for each interceptor */ +InterceptorManager.prototype.forEach = function forEach(fn) { + utils.forEach(this.handlers, function forEachHandler(h) { + if (h !== null) { + fn(h); + } + }); +}; +module.exports = InterceptorManager; /***/ }), -/* 521 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 523 */ +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(522); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(737); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(746); -/* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); -/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); -/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(251); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(248); -/* - * 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. - */ - - - - - +var utils = __webpack_require__(518); +var transformData = __webpack_require__(524); +var isCancel = __webpack_require__(525); +var defaults = __webpack_require__(526); - -async function buildBazelProductionProjects({ - kibanaRoot, - buildRoot, - onlyOSS -}) { - const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_8__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); - const projectNames = [...projects.values()].map(project => project.name); - _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); - await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_4__["runBazel"])(['build', '//packages:build']); - _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); - - for (const project of projects.values()) { - await copyToBuild(project, kibanaRoot, buildRoot); - await applyCorrectPermissions(project, kibanaRoot, buildRoot); +/** + * Throws a `Cancel` if cancellation has been requested. + */ +function throwIfCancellationRequested(config) { + if (config.cancelToken) { + config.cancelToken.throwIfRequested(); } } + /** - * Copy all the project's files from its Bazel dist directory into the - * project build folder. + * Dispatch a request to the server using the configured adapter. * - * When copying all the files into the build, we exclude `node_modules` because - * we want the Kibana build to be responsible for actually installing all - * dependencies. The primary reason for allowing the Kibana build process to - * manage dependencies is that it will "dedupe" them, so we don't include - * unnecessary copies of dependencies. We also exclude every related Bazel build - * files in order to get the most cleaner package module we can in the final distributable. + * @param {object} config The config that is to be used for the request + * @returns {Promise} The Promise to be fulfilled */ +module.exports = function dispatchRequest(config) { + throwIfCancellationRequested(config); -async function copyToBuild(project, kibanaRoot, buildRoot) { - // We want the package to have the same relative location within the build - const relativeProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["relative"])(kibanaRoot, project.path); - const buildProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, relativeProjectPath); - await cpy__WEBPACK_IMPORTED_MODULE_0___default()(['**/*'], buildProjectPath, { - cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel', 'bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), - dot: true, - onlyFiles: true, - parents: true - }); // If a project is using an intermediate build directory, we special-case our - // handling of `package.json`, as the project build process might have copied - // (a potentially modified) `package.json` into the intermediate build - // directory already. If so, we want to use that `package.json` as the basis - // for creating the production-ready `package.json`. If it's not present in - // the intermediate build, we fall back to using the project's already defined - // `package.json`. - - const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["readPackageJson"])(buildProjectPath) : project.json; - const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["createProductionPackageJson"])(packageJson); - await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["writePackageJson"])(buildProjectPath, preparedPackageJson); -} + // Ensure headers exist + config.headers = config.headers || {}; -async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { - const relativeProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["relative"])(kibanaRoot, project.path); - const buildProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, relativeProjectPath); - const allPluginPaths = await globby__WEBPACK_IMPORTED_MODULE_1___default()([`**/*`], { - onlyFiles: false, - cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel', 'bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), - dot: true - }); + // Transform request data + config.data = transformData( + config.data, + config.headers, + config.transformRequest + ); - for (const pluginPath of allPluginPaths) { - const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, pluginPath); + // Flatten headers + config.headers = utils.merge( + config.headers.common || {}, + config.headers[config.method] || {}, + config.headers + ); - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o644); + utils.forEach( + ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], + function cleanHeaderConfig(method) { + delete config.headers[method]; } + ); - if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isDirectory"])(resolvedPluginPath)) { - await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o755); + var adapter = config.adapter || defaults.adapter; + + return adapter(config).then(function onAdapterResolution(response) { + throwIfCancellationRequested(config); + + // Transform response data + response.data = transformData( + response.data, + response.headers, + config.transformResponse + ); + + return response; + }, function onAdapterRejection(reason) { + if (!isCancel(reason)) { + throwIfCancellationRequested(config); + + // Transform response data + if (reason && reason.response) { + reason.response.data = transformData( + reason.response.data, + reason.response.headers, + config.transformResponse + ); + } } - } -} + + return Promise.reject(reason); + }); +}; + /***/ }), -/* 522 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const EventEmitter = __webpack_require__(156); -const path = __webpack_require__(4); -const os = __webpack_require__(121); -const pMap = __webpack_require__(523); -const arrify = __webpack_require__(518); -const globby = __webpack_require__(526); -const hasGlob = __webpack_require__(721); -const cpFile = __webpack_require__(723); -const junk = __webpack_require__(733); -const pFilter = __webpack_require__(734); -const CpyError = __webpack_require__(736); -const defaultOptions = { - ignoreJunk: true -}; +var utils = __webpack_require__(518); -class SourceFile { - constructor(relativePath, path) { - this.path = path; - this.relativePath = relativePath; - Object.freeze(this); - } +/** + * Transform the data for a request or a response + * + * @param {Object|String} data The data to be transformed + * @param {Array} headers The headers for the request or response + * @param {Array|Function} fns A single function or Array of functions + * @returns {*} The resulting transformed data + */ +module.exports = function transformData(data, headers, fns) { + /*eslint no-param-reassign:0*/ + utils.forEach(fns, function transform(fn) { + data = fn(data, headers); + }); - get name() { - return path.basename(this.relativePath); - } + return data; +}; - get nameWithoutExtension() { - return path.basename(this.relativePath, path.extname(this.relativePath)); - } - get extension() { - return path.extname(this.relativePath).slice(1); - } -} +/***/ }), +/* 525 */ +/***/ (function(module, exports, __webpack_require__) { -const preprocessSourcePath = (source, options) => path.resolve(options.cwd ? options.cwd : process.cwd(), source); +"use strict"; -const preprocessDestinationPath = (source, destination, options) => { - let basename = path.basename(source); - if (typeof options.rename === 'string') { - basename = options.rename; - } else if (typeof options.rename === 'function') { - basename = options.rename(basename); - } +module.exports = function isCancel(value) { + return !!(value && value.__CANCEL__); +}; - if (options.cwd) { - destination = path.resolve(options.cwd, destination); - } - if (options.parents) { - const dirname = path.dirname(source); - const parsedDirectory = path.parse(dirname); - return path.join(destination, dirname.replace(parsedDirectory.root, path.sep), basename); - } +/***/ }), +/* 526 */ +/***/ (function(module, exports, __webpack_require__) { - return path.join(destination, basename); -}; +"use strict"; -module.exports = (source, destination, { - concurrency = (os.cpus().length || 1) * 2, - ...options -} = {}) => { - const progressEmitter = new EventEmitter(); - options = { - ...defaultOptions, - ...options - }; +var utils = __webpack_require__(518); +var normalizeHeaderName = __webpack_require__(527); - const promise = (async () => { - source = arrify(source); +var DEFAULT_CONTENT_TYPE = { + 'Content-Type': 'application/x-www-form-urlencoded' +}; - if (source.length === 0 || !destination) { - throw new CpyError('`source` and `destination` required'); - } +function setContentTypeIfUnset(headers, value) { + if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) { + headers['Content-Type'] = value; + } +} - const copyStatus = new Map(); - let completedFiles = 0; - let completedSize = 0; +function getDefaultAdapter() { + var adapter; + if (typeof XMLHttpRequest !== 'undefined') { + // For browsers use XHR adapter + adapter = __webpack_require__(528); + } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { + // For node use HTTP adapter + adapter = __webpack_require__(538); + } + return adapter; +} - let files; - try { - files = await globby(source, options); +var defaults = { + adapter: getDefaultAdapter(), - if (options.ignoreJunk) { - files = files.filter(file => junk.not(path.basename(file))); - } - } catch (error) { - throw new CpyError(`Cannot glob \`${source}\`: ${error.message}`, error); - } + transformRequest: [function transformRequest(data, headers) { + normalizeHeaderName(headers, 'Accept'); + normalizeHeaderName(headers, 'Content-Type'); + if (utils.isFormData(data) || + utils.isArrayBuffer(data) || + utils.isBuffer(data) || + utils.isStream(data) || + utils.isFile(data) || + utils.isBlob(data) + ) { + return data; + } + if (utils.isArrayBufferView(data)) { + return data.buffer; + } + if (utils.isURLSearchParams(data)) { + setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8'); + return data.toString(); + } + if (utils.isObject(data)) { + setContentTypeIfUnset(headers, 'application/json;charset=utf-8'); + return JSON.stringify(data); + } + return data; + }], - if (files.length === 0 && !hasGlob(source)) { - throw new CpyError(`Cannot copy \`${source}\`: the file doesn't exist`); - } + transformResponse: [function transformResponse(data) { + /*eslint no-param-reassign:0*/ + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch (e) { /* Ignore */ } + } + return data; + }], - let sources = files.map(sourcePath => new SourceFile(sourcePath, preprocessSourcePath(sourcePath, options))); + /** + * A timeout in milliseconds to abort a request. If set to 0 (default) a + * timeout is not created. + */ + timeout: 0, - if (options.filter !== undefined) { - const filteredSources = await pFilter(sources, options.filter, {concurrency: 1024}); - sources = filteredSources; - } + xsrfCookieName: 'XSRF-TOKEN', + xsrfHeaderName: 'X-XSRF-TOKEN', - if (sources.length === 0) { - progressEmitter.emit('progress', { - totalFiles: 0, - percent: 1, - completedFiles: 0, - completedSize: 0 - }); - } + maxContentLength: -1, + maxBodyLength: -1, - const fileProgressHandler = event => { - const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0}; + validateStatus: function validateStatus(status) { + return status >= 200 && status < 300; + } +}; - if (fileStatus.written !== event.written || fileStatus.percent !== event.percent) { - completedSize -= fileStatus.written; - completedSize += event.written; +defaults.headers = { + common: { + 'Accept': 'application/json, text/plain, */*' + } +}; - if (event.percent === 1 && fileStatus.percent !== 1) { - completedFiles++; - } +utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) { + defaults.headers[method] = {}; +}); - copyStatus.set(event.src, { - written: event.written, - percent: event.percent - }); +utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { + defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE); +}); - progressEmitter.emit('progress', { - totalFiles: files.length, - percent: completedFiles / files.length, - completedFiles, - completedSize - }); - } - }; +module.exports = defaults; - return pMap(sources, async source => { - const to = preprocessDestinationPath(source.relativePath, destination, options); - try { - await cpFile(source.path, to, options).on('progress', fileProgressHandler); - } catch (error) { - throw new CpyError(`Cannot copy from \`${source.relativePath}\` to \`${to}\`: ${error.message}`, error); - } +/***/ }), +/* 527 */ +/***/ (function(module, exports, __webpack_require__) { - return to; - }, {concurrency}); - })(); +"use strict"; - promise.on = (...arguments_) => { - progressEmitter.on(...arguments_); - return promise; - }; - return promise; +var utils = __webpack_require__(518); + +module.exports = function normalizeHeaderName(headers, normalizedName) { + utils.forEach(headers, function processHeader(value, name) { + if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) { + headers[normalizedName] = value; + delete headers[name]; + } + }); }; /***/ }), -/* 523 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(524); -module.exports = async ( - iterable, - mapper, - { - concurrency = Infinity, - stopOnError = true - } = {} -) => { - return new Promise((resolve, reject) => { - if (typeof mapper !== 'function') { - throw new TypeError('Mapper function is required'); - } +var utils = __webpack_require__(518); +var settle = __webpack_require__(529); +var cookies = __webpack_require__(532); +var buildURL = __webpack_require__(521); +var buildFullPath = __webpack_require__(533); +var parseHeaders = __webpack_require__(536); +var isURLSameOrigin = __webpack_require__(537); +var createError = __webpack_require__(530); - if (!(typeof concurrency === 'number' && concurrency >= 1)) { - throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${concurrency}\` (${typeof concurrency})`); - } +module.exports = function xhrAdapter(config) { + return new Promise(function dispatchXhrRequest(resolve, reject) { + var requestData = config.data; + var requestHeaders = config.headers; - const ret = []; - const errors = []; - const iterator = iterable[Symbol.iterator](); - let isRejected = false; - let isIterableDone = false; - let resolvingCount = 0; - let currentIndex = 0; + if (utils.isFormData(requestData)) { + delete requestHeaders['Content-Type']; // Let the browser set it + } - const next = () => { - if (isRejected) { - return; - } + var request = new XMLHttpRequest(); - const nextItem = iterator.next(); - const i = currentIndex; - currentIndex++; + // HTTP basic authentication + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; + requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); + } - if (nextItem.done) { - isIterableDone = true; + var fullPath = buildFullPath(config.baseURL, config.url); + request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); - if (resolvingCount === 0) { - if (!stopOnError && errors.length !== 0) { - reject(new AggregateError(errors)); - } else { - resolve(ret); - } - } + // Set the request timeout in MS + request.timeout = config.timeout; - return; - } + // Listen for ready state + request.onreadystatechange = function handleLoad() { + if (!request || request.readyState !== 4) { + return; + } - resolvingCount++; + // The request errored out and we didn't get a response, this will be + // handled by onerror instead + // With one exception: request that using file: protocol, most browsers + // will return status as 0 even though it's a successful request + if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { + return; + } - (async () => { - try { - const element = await nextItem.value; - ret[i] = await mapper(element, i); - resolvingCount--; - next(); - } catch (error) { - if (stopOnError) { - isRejected = true; - reject(error); - } else { - errors.push(error); - resolvingCount--; - next(); - } - } - })(); - }; + // Prepare the response + var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; + var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; + var response = { + data: responseData, + status: request.status, + statusText: request.statusText, + headers: responseHeaders, + config: config, + request: request + }; - for (let i = 0; i < concurrency; i++) { - next(); + settle(resolve, reject, response); - if (isIterableDone) { - break; - } - } - }); -}; + // Clean up request + request = null; + }; + // Handle browser request cancellation (as opposed to a manual cancellation) + request.onabort = function handleAbort() { + if (!request) { + return; + } -/***/ }), -/* 524 */ -/***/ (function(module, exports, __webpack_require__) { + reject(createError('Request aborted', config, 'ECONNABORTED', request)); -"use strict"; + // Clean up request + request = null; + }; -const indentString = __webpack_require__(525); -const cleanStack = __webpack_require__(244); + // Handle low level network errors + request.onerror = function handleError() { + // Real errors are hidden from us by the browser + // onerror should only fire if it's a network error + reject(createError('Network Error', config, null, request)); -const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); + // Clean up request + request = null; + }; -class AggregateError extends Error { - constructor(errors) { - if (!Array.isArray(errors)) { - throw new TypeError(`Expected input to be an Array, got ${typeof errors}`); - } + // Handle timeout + request.ontimeout = function handleTimeout() { + var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; + if (config.timeoutErrorMessage) { + timeoutErrorMessage = config.timeoutErrorMessage; + } + reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', + request)); - errors = [...errors].map(error => { - if (error instanceof Error) { - return error; - } + // Clean up request + request = null; + }; - if (error !== null && typeof error === 'object') { - // Handle plain error objects with message property and/or possibly other metadata - return Object.assign(new Error(error.message), error); - } + // Add xsrf header + // This is only done if running in a standard browser environment. + // Specifically not if we're in a web worker, or react-native. + if (utils.isStandardBrowserEnv()) { + // Add xsrf header + var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? + cookies.read(config.xsrfCookieName) : + undefined; - return new Error(error); - }); + if (xsrfValue) { + requestHeaders[config.xsrfHeaderName] = xsrfValue; + } + } - let message = errors - .map(error => { - // The `stack` property is not standardized, so we can't assume it exists - return typeof error.stack === 'string' ? cleanInternalStack(cleanStack(error.stack)) : String(error); - }) - .join('\n'); - message = '\n' + indentString(message, 4); - super(message); + // Add headers to the request + if ('setRequestHeader' in request) { + utils.forEach(requestHeaders, function setRequestHeader(val, key) { + if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { + // Remove Content-Type if data is undefined + delete requestHeaders[key]; + } else { + // Otherwise add header to the request + request.setRequestHeader(key, val); + } + }); + } - this.name = 'AggregateError'; + // Add withCredentials to request if needed + if (!utils.isUndefined(config.withCredentials)) { + request.withCredentials = !!config.withCredentials; + } - Object.defineProperty(this, '_errors', {value: errors}); - } + // Add responseType to request if needed + if (config.responseType) { + try { + request.responseType = config.responseType; + } catch (e) { + // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. + // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. + if (config.responseType !== 'json') { + throw e; + } + } + } - * [Symbol.iterator]() { - for (const error of this._errors) { - yield error; - } - } -} + // Handle progress if needed + if (typeof config.onDownloadProgress === 'function') { + request.addEventListener('progress', config.onDownloadProgress); + } -module.exports = AggregateError; + // Not all browsers support upload events + if (typeof config.onUploadProgress === 'function' && request.upload) { + request.upload.addEventListener('progress', config.onUploadProgress); + } + + if (config.cancelToken) { + // Handle cancellation + config.cancelToken.promise.then(function onCanceled(cancel) { + if (!request) { + return; + } + + request.abort(); + reject(cancel); + // Clean up request + request = null; + }); + } + + if (!requestData) { + requestData = null; + } + + // Send the request + request.send(requestData); + }); +}; /***/ }), -/* 525 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = (string, count = 1, options) => { - options = { - indent: ' ', - includeEmptyLines: false, - ...options - }; +var createError = __webpack_require__(530); - if (typeof string !== 'string') { - throw new TypeError( - `Expected \`input\` to be a \`string\`, got \`${typeof string}\`` - ); - } +/** + * Resolve or reject a Promise based on response status. + * + * @param {Function} resolve A function that resolves the promise. + * @param {Function} reject A function that rejects the promise. + * @param {object} response The response. + */ +module.exports = function settle(resolve, reject, response) { + var validateStatus = response.config.validateStatus; + if (!response.status || !validateStatus || validateStatus(response.status)) { + resolve(response); + } else { + reject(createError( + 'Request failed with status code ' + response.status, + response.config, + null, + response.request, + response + )); + } +}; - if (typeof count !== 'number') { - throw new TypeError( - `Expected \`count\` to be a \`number\`, got \`${typeof count}\`` - ); - } - if (typeof options.indent !== 'string') { - throw new TypeError( - `Expected \`options.indent\` to be a \`string\`, got \`${typeof options.indent}\`` - ); - } +/***/ }), +/* 530 */ +/***/ (function(module, exports, __webpack_require__) { - if (count === 0) { - return string; - } +"use strict"; - const regex = options.includeEmptyLines ? /^/gm : /^(?!\s*$)/gm; - return string.replace(regex, options.indent.repeat(count)); +var enhanceError = __webpack_require__(531); + +/** + * Create an Error with the specified message, config, error code, request and response. + * + * @param {string} message The error message. + * @param {Object} config The config. + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [request] The request. + * @param {Object} [response] The response. + * @returns {Error} The created error. + */ +module.exports = function createError(message, config, code, request, response) { + var error = new Error(message); + return enhanceError(error, config, code, request, response); }; /***/ }), -/* 526 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(527); -const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(529); -const dirGlob = __webpack_require__(714); -const gitignore = __webpack_require__(717); -const DEFAULT_FILTER = () => false; +/** + * Update an Error with the specified config, error code, and response. + * + * @param {Error} error The error to update. + * @param {Object} config The config. + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [request] The request. + * @param {Object} [response] The response. + * @returns {Error} The error. + */ +module.exports = function enhanceError(error, config, code, request, response) { + error.config = config; + if (code) { + error.code = code; + } -const isNegative = pattern => pattern[0] === '!'; + error.request = request; + error.response = response; + error.isAxiosError = true; -const assertPatternsInput = patterns => { - if (!patterns.every(x => typeof x === 'string')) { - throw new TypeError('Patterns must be a string or an array of strings'); - } + error.toJSON = function toJSON() { + return { + // Standard + message: this.message, + name: this.name, + // Microsoft + description: this.description, + number: this.number, + // Mozilla + fileName: this.fileName, + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + stack: this.stack, + // Axios + config: this.config, + code: this.code + }; + }; + return error; }; -const checkCwdOption = options => { - if (options && options.cwd && !fs.statSync(options.cwd).isDirectory()) { - throw new Error('The `cwd` option must be a path to a directory'); - } -}; -const generateGlobTasks = (patterns, taskOptions) => { - patterns = arrayUnion([].concat(patterns)); - assertPatternsInput(patterns); - checkCwdOption(taskOptions); +/***/ }), +/* 532 */ +/***/ (function(module, exports, __webpack_require__) { - const globTasks = []; +"use strict"; - taskOptions = Object.assign({ - ignore: [], - expandDirectories: true - }, taskOptions); - patterns.forEach((pattern, i) => { - if (isNegative(pattern)) { - return; - } +var utils = __webpack_require__(518); - const ignore = patterns - .slice(i) - .filter(isNegative) - .map(pattern => pattern.slice(1)); +module.exports = ( + utils.isStandardBrowserEnv() ? - const options = Object.assign({}, taskOptions, { - ignore: taskOptions.ignore.concat(ignore) - }); + // Standard browser envs support document.cookie + (function standardBrowserEnv() { + return { + write: function write(name, value, expires, path, domain, secure) { + var cookie = []; + cookie.push(name + '=' + encodeURIComponent(value)); - globTasks.push({pattern, options}); - }); + if (utils.isNumber(expires)) { + cookie.push('expires=' + new Date(expires).toGMTString()); + } - return globTasks; -}; + if (utils.isString(path)) { + cookie.push('path=' + path); + } -const globDirs = (task, fn) => { - let options = {}; - if (task.options.cwd) { - options.cwd = task.options.cwd; - } + if (utils.isString(domain)) { + cookie.push('domain=' + domain); + } - if (Array.isArray(task.options.expandDirectories)) { - options = Object.assign(options, {files: task.options.expandDirectories}); - } else if (typeof task.options.expandDirectories === 'object') { - options = Object.assign(options, task.options.expandDirectories); - } + if (secure === true) { + cookie.push('secure'); + } - return fn(task.pattern, options); -}; + document.cookie = cookie.join('; '); + }, -const getPattern = (task, fn) => task.options.expandDirectories ? globDirs(task, fn) : [task.pattern]; + read: function read(name) { + var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); + return (match ? decodeURIComponent(match[3]) : null); + }, + + remove: function remove(name) { + this.write(name, '', Date.now() - 86400000); + } + }; + })() : + + // Non standard browser env (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return { + write: function write() {}, + read: function read() { return null; }, + remove: function remove() {} + }; + })() +); + + +/***/ }), +/* 533 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isAbsoluteURL = __webpack_require__(534); +var combineURLs = __webpack_require__(535); + +/** + * Creates a new URL by combining the baseURL with the requestedURL, + * only when the requestedURL is not already an absolute URL. + * If the requestURL is absolute, this function returns the requestedURL untouched. + * + * @param {string} baseURL The base URL + * @param {string} requestedURL Absolute or relative URL to combine + * @returns {string} The combined full path + */ +module.exports = function buildFullPath(baseURL, requestedURL) { + if (baseURL && !isAbsoluteURL(requestedURL)) { + return combineURLs(baseURL, requestedURL); + } + return requestedURL; +}; + + +/***/ }), +/* 534 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * Determines whether the specified URL is absolute + * + * @param {string} url The URL to test + * @returns {boolean} True if the specified URL is absolute, otherwise false + */ +module.exports = function isAbsoluteURL(url) { + // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). + // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed + // by any combination of letters, digits, plus, period, or hyphen. + return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); +}; + + +/***/ }), +/* 535 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * Creates a new URL by combining the specified URLs + * + * @param {string} baseURL The base URL + * @param {string} relativeURL The relative URL + * @returns {string} The combined URL + */ +module.exports = function combineURLs(baseURL, relativeURL) { + return relativeURL + ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') + : baseURL; +}; + + +/***/ }), +/* 536 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(518); + +// Headers whose duplicates are ignored by node +// c.f. https://nodejs.org/api/http.html#http_message_headers +var ignoreDuplicateOf = [ + 'age', 'authorization', 'content-length', 'content-type', 'etag', + 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', + 'last-modified', 'location', 'max-forwards', 'proxy-authorization', + 'referer', 'retry-after', 'user-agent' +]; + +/** + * Parse headers into an object + * + * ``` + * Date: Wed, 27 Aug 2014 08:58:49 GMT + * Content-Type: application/json + * Connection: keep-alive + * Transfer-Encoding: chunked + * ``` + * + * @param {String} headers Headers needing to be parsed + * @returns {Object} Headers parsed into an object + */ +module.exports = function parseHeaders(headers) { + var parsed = {}; + var key; + var val; + var i; + + if (!headers) { return parsed; } + + utils.forEach(headers.split('\n'), function parser(line) { + i = line.indexOf(':'); + key = utils.trim(line.substr(0, i)).toLowerCase(); + val = utils.trim(line.substr(i + 1)); + + if (key) { + if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) { + return; + } + if (key === 'set-cookie') { + parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]); + } else { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } + } + }); + + return parsed; +}; + + +/***/ }), +/* 537 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(518); + +module.exports = ( + utils.isStandardBrowserEnv() ? + + // Standard browser envs have full support of the APIs needed to test + // whether the request URL is of the same origin as current location. + (function standardBrowserEnv() { + var msie = /(msie|trident)/i.test(navigator.userAgent); + var urlParsingNode = document.createElement('a'); + var originURL; + + /** + * Parse a URL to discover it's components + * + * @param {String} url The URL to be parsed + * @returns {Object} + */ + function resolveURL(url) { + var href = url; + + if (msie) { + // IE needs attribute set twice to normalize properties + urlParsingNode.setAttribute('href', href); + href = urlParsingNode.href; + } + + urlParsingNode.setAttribute('href', href); + + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') ? + urlParsingNode.pathname : + '/' + urlParsingNode.pathname + }; + } + + originURL = resolveURL(window.location.href); + + /** + * Determine if a URL shares the same origin as the current location + * + * @param {String} requestURL The URL to test + * @returns {boolean} True if URL shares the same origin, otherwise false + */ + return function isURLSameOrigin(requestURL) { + var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL; + return (parsed.protocol === originURL.protocol && + parsed.host === originURL.host); + }; + })() : + + // Non standard browser envs (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return function isURLSameOrigin() { + return true; + }; + })() +); + + +/***/ }), +/* 538 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(518); +var settle = __webpack_require__(529); +var buildFullPath = __webpack_require__(533); +var buildURL = __webpack_require__(521); +var http = __webpack_require__(539); +var https = __webpack_require__(540); +var httpFollow = __webpack_require__(541).http; +var httpsFollow = __webpack_require__(541).https; +var url = __webpack_require__(283); +var zlib = __webpack_require__(549); +var pkg = __webpack_require__(550); +var createError = __webpack_require__(530); +var enhanceError = __webpack_require__(531); + +var isHttps = /https:?/; + +/** + * + * @param {http.ClientRequestArgs} options + * @param {AxiosProxyConfig} proxy + * @param {string} location + */ +function setProxy(options, proxy, location) { + options.hostname = proxy.host; + options.host = proxy.host; + options.port = proxy.port; + options.path = location; + + // Basic proxy authorization + if (proxy.auth) { + var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64'); + options.headers['Proxy-Authorization'] = 'Basic ' + base64; + } + + // If a proxy is used, any redirects must also pass through the proxy + options.beforeRedirect = function beforeRedirect(redirection) { + redirection.headers.host = redirection.host; + setProxy(redirection, proxy, redirection.href); + }; +} + +/*eslint consistent-return:0*/ +module.exports = function httpAdapter(config) { + return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { + var resolve = function resolve(value) { + resolvePromise(value); + }; + var reject = function reject(value) { + rejectPromise(value); + }; + var data = config.data; + var headers = config.headers; + + // Set User-Agent (required by some servers) + // Only set header if it hasn't been set in config + // See https://github.com/axios/axios/issues/69 + if (!headers['User-Agent'] && !headers['user-agent']) { + headers['User-Agent'] = 'axios/' + pkg.version; + } + + if (data && !utils.isStream(data)) { + if (Buffer.isBuffer(data)) { + // Nothing to do... + } else if (utils.isArrayBuffer(data)) { + data = Buffer.from(new Uint8Array(data)); + } else if (utils.isString(data)) { + data = Buffer.from(data, 'utf-8'); + } else { + return reject(createError( + 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', + config + )); + } + + // Add Content-Length header if data exists + headers['Content-Length'] = data.length; + } + + // HTTP basic authentication + var auth = undefined; + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password || ''; + auth = username + ':' + password; + } + + // Parse url + var fullPath = buildFullPath(config.baseURL, config.url); + var parsed = url.parse(fullPath); + var protocol = parsed.protocol || 'http:'; + + if (!auth && parsed.auth) { + var urlAuth = parsed.auth.split(':'); + var urlUsername = urlAuth[0] || ''; + var urlPassword = urlAuth[1] || ''; + auth = urlUsername + ':' + urlPassword; + } + + if (auth) { + delete headers.Authorization; + } + + var isHttpsRequest = isHttps.test(protocol); + var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; + + var options = { + path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), + method: config.method.toUpperCase(), + headers: headers, + agent: agent, + agents: { http: config.httpAgent, https: config.httpsAgent }, + auth: auth + }; + + if (config.socketPath) { + options.socketPath = config.socketPath; + } else { + options.hostname = parsed.hostname; + options.port = parsed.port; + } + + var proxy = config.proxy; + if (!proxy && proxy !== false) { + var proxyEnv = protocol.slice(0, -1) + '_proxy'; + var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()]; + if (proxyUrl) { + var parsedProxyUrl = url.parse(proxyUrl); + var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY; + var shouldProxy = true; + + if (noProxyEnv) { + var noProxy = noProxyEnv.split(',').map(function trim(s) { + return s.trim(); + }); + + shouldProxy = !noProxy.some(function proxyMatch(proxyElement) { + if (!proxyElement) { + return false; + } + if (proxyElement === '*') { + return true; + } + if (proxyElement[0] === '.' && + parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement) { + return true; + } + + return parsed.hostname === proxyElement; + }); + } + + if (shouldProxy) { + proxy = { + host: parsedProxyUrl.hostname, + port: parsedProxyUrl.port, + protocol: parsedProxyUrl.protocol + }; + + if (parsedProxyUrl.auth) { + var proxyUrlAuth = parsedProxyUrl.auth.split(':'); + proxy.auth = { + username: proxyUrlAuth[0], + password: proxyUrlAuth[1] + }; + } + } + } + } + + if (proxy) { + options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : ''); + setProxy(options, proxy, protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path); + } + + var transport; + var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true); + if (config.transport) { + transport = config.transport; + } else if (config.maxRedirects === 0) { + transport = isHttpsProxy ? https : http; + } else { + if (config.maxRedirects) { + options.maxRedirects = config.maxRedirects; + } + transport = isHttpsProxy ? httpsFollow : httpFollow; + } + + if (config.maxBodyLength > -1) { + options.maxBodyLength = config.maxBodyLength; + } + + // Create the request + var req = transport.request(options, function handleResponse(res) { + if (req.aborted) return; + + // uncompress the response body transparently if required + var stream = res; + + // return the last request in case of redirects + var lastRequest = res.req || req; + + + // if no content, is HEAD request or decompress disabled we should not decompress + if (res.statusCode !== 204 && lastRequest.method !== 'HEAD' && config.decompress !== false) { + switch (res.headers['content-encoding']) { + /*eslint default-case:0*/ + case 'gzip': + case 'compress': + case 'deflate': + // add the unzipper to the body stream processing pipeline + stream = stream.pipe(zlib.createUnzip()); + + // remove the content-encoding in order to not confuse downstream operations + delete res.headers['content-encoding']; + break; + } + } + + var response = { + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + config: config, + request: lastRequest + }; + + if (config.responseType === 'stream') { + response.data = stream; + settle(resolve, reject, response); + } else { + var responseBuffer = []; + stream.on('data', function handleStreamData(chunk) { + responseBuffer.push(chunk); + + // make sure the content length is not over the maxContentLength if specified + if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { + stream.destroy(); + reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', + config, null, lastRequest)); + } + }); + + stream.on('error', function handleStreamError(err) { + if (req.aborted) return; + reject(enhanceError(err, config, null, lastRequest)); + }); + + stream.on('end', function handleStreamEnd() { + var responseData = Buffer.concat(responseBuffer); + if (config.responseType !== 'arraybuffer') { + responseData = responseData.toString(config.responseEncoding); + if (!config.responseEncoding || config.responseEncoding === 'utf8') { + responseData = utils.stripBOM(responseData); + } + } + + response.data = responseData; + settle(resolve, reject, response); + }); + } + }); + + // Handle errors + req.on('error', function handleRequestError(err) { + if (req.aborted && err.code !== 'ERR_FR_TOO_MANY_REDIRECTS') return; + reject(enhanceError(err, config, null, req)); + }); + + // Handle request timeout + if (config.timeout) { + // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system. + // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET. + // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. + // And then these socket which be hang up will devoring CPU little by little. + // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. + req.setTimeout(config.timeout, function handleRequestTimeout() { + req.abort(); + reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); + }); + } + + if (config.cancelToken) { + // Handle cancellation + config.cancelToken.promise.then(function onCanceled(cancel) { + if (req.aborted) return; + + req.abort(); + reject(cancel); + }); + } + + // Send the request + if (utils.isStream(data)) { + data.on('error', function handleStreamError(err) { + reject(enhanceError(err, config, null, req)); + }).pipe(req); + } else { + req.end(data); + } + }); +}; + + +/***/ }), +/* 539 */ +/***/ (function(module, exports) { + +module.exports = require("http"); + +/***/ }), +/* 540 */ +/***/ (function(module, exports) { + +module.exports = require("https"); + +/***/ }), +/* 541 */ +/***/ (function(module, exports, __webpack_require__) { + +var url = __webpack_require__(283); +var URL = url.URL; +var http = __webpack_require__(539); +var https = __webpack_require__(540); +var Writable = __webpack_require__(138).Writable; +var assert = __webpack_require__(140); +var debug = __webpack_require__(542); + +// Create handlers that pass events from native requests +var eventHandlers = Object.create(null); +["abort", "aborted", "connect", "error", "socket", "timeout"].forEach(function (event) { + eventHandlers[event] = function (arg1, arg2, arg3) { + this._redirectable.emit(event, arg1, arg2, arg3); + }; +}); + +// Error types with codes +var RedirectionError = createErrorType( + "ERR_FR_REDIRECTION_FAILURE", + "" +); +var TooManyRedirectsError = createErrorType( + "ERR_FR_TOO_MANY_REDIRECTS", + "Maximum number of redirects exceeded" +); +var MaxBodyLengthExceededError = createErrorType( + "ERR_FR_MAX_BODY_LENGTH_EXCEEDED", + "Request body larger than maxBodyLength limit" +); +var WriteAfterEndError = createErrorType( + "ERR_STREAM_WRITE_AFTER_END", + "write after end" +); + +// An HTTP(S) request that can be redirected +function RedirectableRequest(options, responseCallback) { + // Initialize the request + Writable.call(this); + this._sanitizeOptions(options); + this._options = options; + this._ended = false; + this._ending = false; + this._redirectCount = 0; + this._redirects = []; + this._requestBodyLength = 0; + this._requestBodyBuffers = []; + + // Attach a callback if passed + if (responseCallback) { + this.on("response", responseCallback); + } + + // React to responses of native requests + var self = this; + this._onNativeResponse = function (response) { + self._processResponse(response); + }; + + // Perform the first request + this._performRequest(); +} +RedirectableRequest.prototype = Object.create(Writable.prototype); + +// Writes buffered data to the current native request +RedirectableRequest.prototype.write = function (data, encoding, callback) { + // Writing is not allowed if end has been called + if (this._ending) { + throw new WriteAfterEndError(); + } + + // Validate input and shift parameters if necessary + if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) { + throw new TypeError("data should be a string, Buffer or Uint8Array"); + } + if (typeof encoding === "function") { + callback = encoding; + encoding = null; + } + + // Ignore empty buffers, since writing them doesn't invoke the callback + // https://github.com/nodejs/node/issues/22066 + if (data.length === 0) { + if (callback) { + callback(); + } + return; + } + // Only write when we don't exceed the maximum body length + if (this._requestBodyLength + data.length <= this._options.maxBodyLength) { + this._requestBodyLength += data.length; + this._requestBodyBuffers.push({ data: data, encoding: encoding }); + this._currentRequest.write(data, encoding, callback); + } + // Error when we exceed the maximum body length + else { + this.emit("error", new MaxBodyLengthExceededError()); + this.abort(); + } +}; + +// Ends the current native request +RedirectableRequest.prototype.end = function (data, encoding, callback) { + // Shift parameters if necessary + if (typeof data === "function") { + callback = data; + data = encoding = null; + } + else if (typeof encoding === "function") { + callback = encoding; + encoding = null; + } + + // Write data if needed and end + if (!data) { + this._ended = this._ending = true; + this._currentRequest.end(null, null, callback); + } + else { + var self = this; + var currentRequest = this._currentRequest; + this.write(data, encoding, function () { + self._ended = true; + currentRequest.end(null, null, callback); + }); + this._ending = true; + } +}; + +// Sets a header value on the current native request +RedirectableRequest.prototype.setHeader = function (name, value) { + this._options.headers[name] = value; + this._currentRequest.setHeader(name, value); +}; + +// Clears a header value on the current native request +RedirectableRequest.prototype.removeHeader = function (name) { + delete this._options.headers[name]; + this._currentRequest.removeHeader(name); +}; + +// Global timeout for all underlying requests +RedirectableRequest.prototype.setTimeout = function (msecs, callback) { + if (callback) { + this.once("timeout", callback); + } + + if (this.socket) { + startTimer(this, msecs); + } + else { + var self = this; + this._currentRequest.once("socket", function () { + startTimer(self, msecs); + }); + } + + this.once("response", clearTimer); + this.once("error", clearTimer); + + return this; +}; + +function startTimer(request, msecs) { + clearTimeout(request._timeout); + request._timeout = setTimeout(function () { + request.emit("timeout"); + }, msecs); +} + +function clearTimer() { + clearTimeout(this._timeout); +} + +// Proxy all other public ClientRequest methods +[ + "abort", "flushHeaders", "getHeader", + "setNoDelay", "setSocketKeepAlive", +].forEach(function (method) { + RedirectableRequest.prototype[method] = function (a, b) { + return this._currentRequest[method](a, b); + }; +}); + +// Proxy all public ClientRequest properties +["aborted", "connection", "socket"].forEach(function (property) { + Object.defineProperty(RedirectableRequest.prototype, property, { + get: function () { return this._currentRequest[property]; }, + }); +}); + +RedirectableRequest.prototype._sanitizeOptions = function (options) { + // Ensure headers are always present + if (!options.headers) { + options.headers = {}; + } + + // Since http.request treats host as an alias of hostname, + // but the url module interprets host as hostname plus port, + // eliminate the host property to avoid confusion. + if (options.host) { + // Use hostname if set, because it has precedence + if (!options.hostname) { + options.hostname = options.host; + } + delete options.host; + } + + // Complete the URL object when necessary + if (!options.pathname && options.path) { + var searchPos = options.path.indexOf("?"); + if (searchPos < 0) { + options.pathname = options.path; + } + else { + options.pathname = options.path.substring(0, searchPos); + options.search = options.path.substring(searchPos); + } + } +}; + + +// Executes the next native request (initial or redirect) +RedirectableRequest.prototype._performRequest = function () { + // Load the native protocol + var protocol = this._options.protocol; + var nativeProtocol = this._options.nativeProtocols[protocol]; + if (!nativeProtocol) { + this.emit("error", new TypeError("Unsupported protocol " + protocol)); + return; + } + + // If specified, use the agent corresponding to the protocol + // (HTTP and HTTPS use different types of agents) + if (this._options.agents) { + var scheme = protocol.substr(0, protocol.length - 1); + this._options.agent = this._options.agents[scheme]; + } + + // Create the native request + var request = this._currentRequest = + nativeProtocol.request(this._options, this._onNativeResponse); + this._currentUrl = url.format(this._options); + + // Set up event handlers + request._redirectable = this; + for (var event in eventHandlers) { + /* istanbul ignore else */ + if (event) { + request.on(event, eventHandlers[event]); + } + } + + // End a redirected request + // (The first request must be ended explicitly with RedirectableRequest#end) + if (this._isRedirect) { + // Write the request entity and end. + var i = 0; + var self = this; + var buffers = this._requestBodyBuffers; + (function writeNext(error) { + // Only write if this request has not been redirected yet + /* istanbul ignore else */ + if (request === self._currentRequest) { + // Report any write errors + /* istanbul ignore if */ + if (error) { + self.emit("error", error); + } + // Write the next buffer if there are still left + else if (i < buffers.length) { + var buffer = buffers[i++]; + /* istanbul ignore else */ + if (!request.finished) { + request.write(buffer.data, buffer.encoding, writeNext); + } + } + // End the request if `end` has been called on us + else if (self._ended) { + request.end(); + } + } + }()); + } +}; + +// Processes a response from the current native request +RedirectableRequest.prototype._processResponse = function (response) { + // Store the redirected response + var statusCode = response.statusCode; + if (this._options.trackRedirects) { + this._redirects.push({ + url: this._currentUrl, + headers: response.headers, + statusCode: statusCode, + }); + } + + // RFC7231§6.4: The 3xx (Redirection) class of status code indicates + // that further action needs to be taken by the user agent in order to + // fulfill the request. If a Location header field is provided, + // the user agent MAY automatically redirect its request to the URI + // referenced by the Location field value, + // even if the specific status code is not understood. + var location = response.headers.location; + if (location && this._options.followRedirects !== false && + statusCode >= 300 && statusCode < 400) { + // Abort the current request + this._currentRequest.removeAllListeners(); + this._currentRequest.on("error", noop); + this._currentRequest.abort(); + // Discard the remainder of the response to avoid waiting for data + response.destroy(); + + // RFC7231§6.4: A client SHOULD detect and intervene + // in cyclical redirections (i.e., "infinite" redirection loops). + if (++this._redirectCount > this._options.maxRedirects) { + this.emit("error", new TooManyRedirectsError()); + return; + } + + // RFC7231§6.4: Automatic redirection needs to done with + // care for methods not known to be safe, […] + // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change + // the request method from POST to GET for the subsequent request. + if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" || + // RFC7231§6.4.4: The 303 (See Other) status code indicates that + // the server is redirecting the user agent to a different resource […] + // A user agent can perform a retrieval request targeting that URI + // (a GET or HEAD request if using HTTP) […] + (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) { + this._options.method = "GET"; + // Drop a possible entity and headers related to it + this._requestBodyBuffers = []; + removeMatchingHeaders(/^content-/i, this._options.headers); + } + + // Drop the Host header, as the redirect might lead to a different host + var previousHostName = removeMatchingHeaders(/^host$/i, this._options.headers) || + url.parse(this._currentUrl).hostname; + + // Create the redirected request + var redirectUrl = url.resolve(this._currentUrl, location); + debug("redirecting to", redirectUrl); + this._isRedirect = true; + var redirectUrlParts = url.parse(redirectUrl); + Object.assign(this._options, redirectUrlParts); + + // Drop the Authorization header if redirecting to another host + if (redirectUrlParts.hostname !== previousHostName) { + removeMatchingHeaders(/^authorization$/i, this._options.headers); + } + + // Evaluate the beforeRedirect callback + if (typeof this._options.beforeRedirect === "function") { + var responseDetails = { headers: response.headers }; + try { + this._options.beforeRedirect.call(null, this._options, responseDetails); + } + catch (err) { + this.emit("error", err); + return; + } + this._sanitizeOptions(this._options); + } + + // Perform the redirected request + try { + this._performRequest(); + } + catch (cause) { + var error = new RedirectionError("Redirected request failed: " + cause.message); + error.cause = cause; + this.emit("error", error); + } + } + else { + // The response is not a redirect; return it as-is + response.responseUrl = this._currentUrl; + response.redirects = this._redirects; + this.emit("response", response); + + // Clean up + this._requestBodyBuffers = []; + } +}; + +// Wraps the key/value object of protocols with redirect functionality +function wrap(protocols) { + // Default settings + var exports = { + maxRedirects: 21, + maxBodyLength: 10 * 1024 * 1024, + }; + + // Wrap each protocol + var nativeProtocols = {}; + Object.keys(protocols).forEach(function (scheme) { + var protocol = scheme + ":"; + var nativeProtocol = nativeProtocols[protocol] = protocols[scheme]; + var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); + + // Executes a request, following redirects + wrappedProtocol.request = function (input, options, callback) { + // Parse parameters + if (typeof input === "string") { + var urlStr = input; + try { + input = urlToOptions(new URL(urlStr)); + } + catch (err) { + /* istanbul ignore next */ + input = url.parse(urlStr); + } + } + else if (URL && (input instanceof URL)) { + input = urlToOptions(input); + } + else { + callback = options; + options = input; + input = { protocol: protocol }; + } + if (typeof options === "function") { + callback = options; + options = null; + } + + // Set defaults + options = Object.assign({ + maxRedirects: exports.maxRedirects, + maxBodyLength: exports.maxBodyLength, + }, input, options); + options.nativeProtocols = nativeProtocols; + + assert.equal(options.protocol, protocol, "protocol mismatch"); + debug("options", options); + return new RedirectableRequest(options, callback); + }; + + // Executes a GET request, following redirects + wrappedProtocol.get = function (input, options, callback) { + var request = wrappedProtocol.request(input, options, callback); + request.end(); + return request; + }; + }); + return exports; +} + +/* istanbul ignore next */ +function noop() { /* empty */ } + +// from https://github.com/nodejs/node/blob/master/lib/internal/url.js +function urlToOptions(urlObject) { + var options = { + protocol: urlObject.protocol, + hostname: urlObject.hostname.startsWith("[") ? + /* istanbul ignore next */ + urlObject.hostname.slice(1, -1) : + urlObject.hostname, + hash: urlObject.hash, + search: urlObject.search, + pathname: urlObject.pathname, + path: urlObject.pathname + urlObject.search, + href: urlObject.href, + }; + if (urlObject.port !== "") { + options.port = Number(urlObject.port); + } + return options; +} + +function removeMatchingHeaders(regex, headers) { + var lastValue; + for (var header in headers) { + if (regex.test(header)) { + lastValue = headers[header]; + delete headers[header]; + } + } + return lastValue; +} + +function createErrorType(code, defaultMessage) { + function CustomError(message) { + Error.captureStackTrace(this, this.constructor); + this.message = message || defaultMessage; + } + CustomError.prototype = new Error(); + CustomError.prototype.constructor = CustomError; + CustomError.prototype.name = "Error [" + code + "]"; + CustomError.prototype.code = code; + return CustomError; +} + +// Exports +module.exports = wrap({ http: http, https: https }); +module.exports.wrap = wrap; + + +/***/ }), +/* 542 */ +/***/ (function(module, exports, __webpack_require__) { + +var debug; +try { + /* eslint global-require: off */ + debug = __webpack_require__(543)("follow-redirects"); +} +catch (error) { + debug = function () { /* */ }; +} +module.exports = debug; + + +/***/ }), +/* 543 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * Detect Electron renderer process, which is node, but we should + * treat as a browser. + */ + +if (typeof process !== 'undefined' && process.type === 'renderer') { + module.exports = __webpack_require__(544); +} else { + module.exports = __webpack_require__(547); +} + + +/***/ }), +/* 544 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * This is the web browser implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = __webpack_require__(545); +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; +exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); + +/** + * Colors. + */ + +exports.colors = [ + 'lightseagreen', + 'forestgreen', + 'goldenrod', + 'dodgerblue', + 'darkorchid', + 'crimson' +]; + +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } + + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs(args) { + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return; + + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} + + +/***/ }), +/* 545 */ +/***/ (function(module, exports, __webpack_require__) { + + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = __webpack_require__(546); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + +exports.formatters = {}; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + + +/***/ }), +/* 546 */ +/***/ (function(module, exports) { + +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} + + +/***/ }), +/* 547 */ +/***/ (function(module, exports, __webpack_require__) { + +/** + * Module dependencies. + */ + +var tty = __webpack_require__(122); +var util = __webpack_require__(112); + +/** + * This is the Node.js implementation of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = __webpack_require__(545); +exports.init = init; +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; + +/** + * Colors. + */ + +exports.colors = [6, 2, 3, 4, 5, 1]; + +/** + * Build up the default `inspectOpts` object from the environment variables. + * + * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js + */ + +exports.inspectOpts = Object.keys(process.env).filter(function (key) { + return /^debug_/i.test(key); +}).reduce(function (obj, key) { + // camel-case + var prop = key + .substring(6) + .toLowerCase() + .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); + + // coerce string value into JS value + var val = process.env[key]; + if (/^(yes|on|true|enabled)$/i.test(val)) val = true; + else if (/^(no|off|false|disabled)$/i.test(val)) val = false; + else if (val === 'null') val = null; + else val = Number(val); + + obj[prop] = val; + return obj; +}, {}); + +/** + * The file descriptor to write the `debug()` calls to. + * Set the `DEBUG_FD` env variable to override with another value. i.e.: + * + * $ DEBUG_FD=3 node script.js 3>debug.log + */ + +var fd = parseInt(process.env.DEBUG_FD, 10) || 2; + +if (1 !== fd && 2 !== fd) { + util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')() +} + +var stream = 1 === fd ? process.stdout : + 2 === fd ? process.stderr : + createWritableStdioStream(fd); + +/** + * Is stdout a TTY? Colored output is enabled when `true`. + */ + +function useColors() { + return 'colors' in exports.inspectOpts + ? Boolean(exports.inspectOpts.colors) + : tty.isatty(fd); +} + +/** + * Map %o to `util.inspect()`, all on a single line. + */ + +exports.formatters.o = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts) + .split('\n').map(function(str) { + return str.trim() + }).join(' '); +}; + +/** + * Map %o to `util.inspect()`, allowing multiple lines if needed. + */ + +exports.formatters.O = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts); +}; + +/** + * Adds ANSI color escape codes if enabled. + * + * @api public + */ + +function formatArgs(args) { + var name = this.namespace; + var useColors = this.useColors; + + if (useColors) { + var c = this.color; + var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m'; + + args[0] = prefix + args[0].split('\n').join('\n' + prefix); + args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); + } else { + args[0] = new Date().toUTCString() + + ' ' + name + ' ' + args[0]; + } +} + +/** + * Invokes `util.format()` with the specified arguments and writes to `stream`. + */ + +function log() { + return stream.write(util.format.apply(util, arguments) + '\n'); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + if (null == namespaces) { + // If you set a process.env field to null or undefined, it gets cast to the + // string 'null' or 'undefined'. Just delete instead. + delete process.env.DEBUG; + } else { + process.env.DEBUG = namespaces; + } +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + return process.env.DEBUG; +} + +/** + * Copied from `node/src/node.js`. + * + * XXX: It's lame that node doesn't expose this API out-of-the-box. It also + * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. + */ + +function createWritableStdioStream (fd) { + var stream; + var tty_wrap = process.binding('tty_wrap'); + + // Note stream._type is used for test-module-load-list.js + + switch (tty_wrap.guessHandleType(fd)) { + case 'TTY': + stream = new tty.WriteStream(fd); + stream._type = 'tty'; + + // Hack to have stream not keep the event loop alive. + // See https://github.com/joyent/node/issues/1726 + if (stream._handle && stream._handle.unref) { + stream._handle.unref(); + } + break; + + case 'FILE': + var fs = __webpack_require__(134); + stream = new fs.SyncWriteStream(fd, { autoClose: false }); + stream._type = 'fs'; + break; + + case 'PIPE': + case 'TCP': + var net = __webpack_require__(548); + stream = new net.Socket({ + fd: fd, + readable: false, + writable: true + }); + + // FIXME Should probably have an option in net.Socket to create a + // stream from an existing fd which is writable only. But for now + // we'll just add this hack and set the `readable` member to false. + // Test: ./node test/fixtures/echo.js < /etc/passwd + stream.readable = false; + stream.read = null; + stream._type = 'pipe'; + + // FIXME Hack to have stream not keep the event loop alive. + // See https://github.com/joyent/node/issues/1726 + if (stream._handle && stream._handle.unref) { + stream._handle.unref(); + } + break; + + default: + // Probably an error on in uv_guess_handle() + throw new Error('Implement me. Unknown stream file type!'); + } + + // For supporting legacy API we put the FD here. + stream.fd = fd; + + stream._isStdio = true; + + return stream; +} + +/** + * Init logic for `debug` instances. + * + * Create a new `inspectOpts` object in case `useColors` is set + * differently for a particular `debug` instance. + */ + +function init (debug) { + debug.inspectOpts = {}; + + var keys = Object.keys(exports.inspectOpts); + for (var i = 0; i < keys.length; i++) { + debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; + } +} + +/** + * Enable namespaces listed in `process.env.DEBUG` initially. + */ + +exports.enable(load()); + + +/***/ }), +/* 548 */ +/***/ (function(module, exports) { + +module.exports = require("net"); + +/***/ }), +/* 549 */ +/***/ (function(module, exports) { + +module.exports = require("zlib"); + +/***/ }), +/* 550 */ +/***/ (function(module) { + +module.exports = JSON.parse("{\"name\":\"axios\",\"version\":\"0.21.1\",\"description\":\"Promise based HTTP client for the browser and node.js\",\"main\":\"index.js\",\"scripts\":{\"test\":\"grunt test && bundlesize\",\"start\":\"node ./sandbox/server.js\",\"build\":\"NODE_ENV=production grunt build\",\"preversion\":\"npm test\",\"version\":\"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json\",\"postversion\":\"git push && git push --tags\",\"examples\":\"node ./examples/server.js\",\"coveralls\":\"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js\",\"fix\":\"eslint --fix lib/**/*.js\"},\"repository\":{\"type\":\"git\",\"url\":\"https://github.com/axios/axios.git\"},\"keywords\":[\"xhr\",\"http\",\"ajax\",\"promise\",\"node\"],\"author\":\"Matt Zabriskie\",\"license\":\"MIT\",\"bugs\":{\"url\":\"https://github.com/axios/axios/issues\"},\"homepage\":\"https://github.com/axios/axios\",\"devDependencies\":{\"bundlesize\":\"^0.17.0\",\"coveralls\":\"^3.0.0\",\"es6-promise\":\"^4.2.4\",\"grunt\":\"^1.0.2\",\"grunt-banner\":\"^0.6.0\",\"grunt-cli\":\"^1.2.0\",\"grunt-contrib-clean\":\"^1.1.0\",\"grunt-contrib-watch\":\"^1.0.0\",\"grunt-eslint\":\"^20.1.0\",\"grunt-karma\":\"^2.0.0\",\"grunt-mocha-test\":\"^0.13.3\",\"grunt-ts\":\"^6.0.0-beta.19\",\"grunt-webpack\":\"^1.0.18\",\"istanbul-instrumenter-loader\":\"^1.0.0\",\"jasmine-core\":\"^2.4.1\",\"karma\":\"^1.3.0\",\"karma-chrome-launcher\":\"^2.2.0\",\"karma-coverage\":\"^1.1.1\",\"karma-firefox-launcher\":\"^1.1.0\",\"karma-jasmine\":\"^1.1.1\",\"karma-jasmine-ajax\":\"^0.1.13\",\"karma-opera-launcher\":\"^1.0.0\",\"karma-safari-launcher\":\"^1.0.0\",\"karma-sauce-launcher\":\"^1.2.0\",\"karma-sinon\":\"^1.0.5\",\"karma-sourcemap-loader\":\"^0.3.7\",\"karma-webpack\":\"^1.7.0\",\"load-grunt-tasks\":\"^3.5.2\",\"minimist\":\"^1.2.0\",\"mocha\":\"^5.2.0\",\"sinon\":\"^4.5.0\",\"typescript\":\"^2.8.1\",\"url-search-params\":\"^0.10.0\",\"webpack\":\"^1.13.1\",\"webpack-dev-server\":\"^1.14.1\"},\"browser\":{\"./lib/adapters/http.js\":\"./lib/adapters/xhr.js\"},\"jsdelivr\":\"dist/axios.min.js\",\"unpkg\":\"dist/axios.min.js\",\"typings\":\"./index.d.ts\",\"dependencies\":{\"follow-redirects\":\"^1.10.0\"},\"bundlesize\":[{\"path\":\"./dist/axios.min.js\",\"threshold\":\"5kB\"}]}"); + +/***/ }), +/* 551 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(518); + +/** + * Config-specific merge-function which creates a new config-object + * by merging two configuration objects together. + * + * @param {Object} config1 + * @param {Object} config2 + * @returns {Object} New object resulting from merging config2 to config1 + */ +module.exports = function mergeConfig(config1, config2) { + // eslint-disable-next-line no-param-reassign + config2 = config2 || {}; + var config = {}; + + var valueFromConfig2Keys = ['url', 'method', 'data']; + var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params']; + var defaultToConfig2Keys = [ + 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', + 'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', + 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress', + 'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent', + 'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding' + ]; + var directMergeKeys = ['validateStatus']; + + function getMergedValue(target, source) { + if (utils.isPlainObject(target) && utils.isPlainObject(source)) { + return utils.merge(target, source); + } else if (utils.isPlainObject(source)) { + return utils.merge({}, source); + } else if (utils.isArray(source)) { + return source.slice(); + } + return source; + } + + function mergeDeepProperties(prop) { + if (!utils.isUndefined(config2[prop])) { + config[prop] = getMergedValue(config1[prop], config2[prop]); + } else if (!utils.isUndefined(config1[prop])) { + config[prop] = getMergedValue(undefined, config1[prop]); + } + } + + utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { + if (!utils.isUndefined(config2[prop])) { + config[prop] = getMergedValue(undefined, config2[prop]); + } + }); + + utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties); + + utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { + if (!utils.isUndefined(config2[prop])) { + config[prop] = getMergedValue(undefined, config2[prop]); + } else if (!utils.isUndefined(config1[prop])) { + config[prop] = getMergedValue(undefined, config1[prop]); + } + }); + + utils.forEach(directMergeKeys, function merge(prop) { + if (prop in config2) { + config[prop] = getMergedValue(config1[prop], config2[prop]); + } else if (prop in config1) { + config[prop] = getMergedValue(undefined, config1[prop]); + } + }); + + var axiosKeys = valueFromConfig2Keys + .concat(mergeDeepPropertiesKeys) + .concat(defaultToConfig2Keys) + .concat(directMergeKeys); + + var otherKeys = Object + .keys(config1) + .concat(Object.keys(config2)) + .filter(function filterAxiosKeys(key) { + return axiosKeys.indexOf(key) === -1; + }); + + utils.forEach(otherKeys, mergeDeepProperties); + + return config; +}; + + +/***/ }), +/* 552 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * A `Cancel` is an object that is thrown when an operation is canceled. + * + * @class + * @param {string=} message The message. + */ +function Cancel(message) { + this.message = message; +} + +Cancel.prototype.toString = function toString() { + return 'Cancel' + (this.message ? ': ' + this.message : ''); +}; + +Cancel.prototype.__CANCEL__ = true; + +module.exports = Cancel; + + +/***/ }), +/* 553 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var Cancel = __webpack_require__(552); + +/** + * A `CancelToken` is an object that can be used to request cancellation of an operation. + * + * @class + * @param {Function} executor The executor function. + */ +function CancelToken(executor) { + if (typeof executor !== 'function') { + throw new TypeError('executor must be a function.'); + } + + var resolvePromise; + this.promise = new Promise(function promiseExecutor(resolve) { + resolvePromise = resolve; + }); + + var token = this; + executor(function cancel(message) { + if (token.reason) { + // Cancellation has already been requested + return; + } + + token.reason = new Cancel(message); + resolvePromise(token.reason); + }); +} + +/** + * Throws a `Cancel` if cancellation has been requested. + */ +CancelToken.prototype.throwIfRequested = function throwIfRequested() { + if (this.reason) { + throw this.reason; + } +}; + +/** + * Returns an object that contains a new `CancelToken` and a function that, when called, + * cancels the `CancelToken`. + */ +CancelToken.source = function source() { + var cancel; + var token = new CancelToken(function executor(c) { + cancel = c; + }); + return { + token: token, + cancel: cancel + }; +}; + +module.exports = CancelToken; + + +/***/ }), +/* 554 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * Syntactic sugar for invoking a function and expanding an array for arguments. + * + * Common use case would be to use `Function.prototype.apply`. + * + * ```js + * function f(x, y, z) {} + * var args = [1, 2, 3]; + * f.apply(null, args); + * ``` + * + * With `spread` this example can be re-written. + * + * ```js + * spread(function(x, y, z) {})([1, 2, 3]); + * ``` + * + * @param {Function} callback + * @returns {Function} + */ +module.exports = function spread(callback) { + return function wrap(arr) { + return callback.apply(null, arr); + }; +}; + + +/***/ }), +/* 555 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * Determines whether the payload is an error thrown by Axios + * + * @param {*} payload The value to test + * @returns {boolean} True if the payload is an error thrown by Axios, otherwise false + */ +module.exports = function isAxiosError(payload) { + return (typeof payload === 'object') && (payload.isAxiosError === true); +}; + + +/***/ }), +/* 556 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.parseConfig = void 0; +function validateConfig(log, config) { + const validApiToken = typeof config.apiToken === 'string' && config.apiToken.length !== 0; + if (!validApiToken) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api token, stats will not be reported'); + return; + } + const validId = typeof config.buildId === 'string' && config.buildId.length !== 0; + if (!validId) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid build id, stats will not be reported'); + return; + } + return config; +} +function parseConfig(log) { + const configJson = process.env.KIBANA_CI_STATS_CONFIG; + if (!configJson) { + log.debug('KIBANA_CI_STATS_CONFIG environment variable not found, disabling CiStatsReporter'); + return; + } + let config; + try { + config = JSON.parse(configJson); + } + catch (_) { + // handled below + } + if (typeof config === 'object' && config !== null) { + return validateConfig(log, config); + } + log.warning('KIBANA_CI_STATS_CONFIG is invalid, stats will not be reported'); + return; +} +exports.parseConfig = parseConfig; + + +/***/ }), +/* 557 */ +/***/ (function(module, exports) { + +function webpackEmptyContext(req) { + var e = new Error("Cannot find module '" + req + "'"); + e.code = 'MODULE_NOT_FOUND'; + throw e; +} +webpackEmptyContext.keys = function() { return []; }; +webpackEmptyContext.resolve = webpackEmptyContext; +module.exports = webpackEmptyContext; +webpackEmptyContext.id = 557; + +/***/ }), +/* 558 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(134); +/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(559); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(239); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(366); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(248); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(562); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +/* + * 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. + */ + + + + + + + +/** + * Helper class for dealing with a set of projects as children of + * the Kibana project. The kbn/pm is currently implemented to be + * more generic, where everything is an operation of generic projects, + * but that leads to exceptions where we need the kibana project and + * do things like `project.get('kibana')!`. + * + * Using this helper we can restructre the generic list of projects + * as a Kibana object which encapulates all the projects in the + * workspace and knows about the root Kibana project. + */ + +class Kibana { + static async loadFrom(rootPath) { + return new Kibana(await Object(_projects__WEBPACK_IMPORTED_MODULE_5__["getProjects"])(rootPath, Object(_config__WEBPACK_IMPORTED_MODULE_6__["getProjectPaths"])({ + rootPath + }))); + } + + constructor(allWorkspaceProjects) { + this.allWorkspaceProjects = allWorkspaceProjects; + + _defineProperty(this, "kibanaProject", void 0); + + const kibanaProject = allWorkspaceProjects.get('kibana'); + + if (!kibanaProject) { + throw new TypeError('Unable to create Kibana object without all projects, including the Kibana project.'); + } + + this.kibanaProject = kibanaProject; + } + /** make an absolute path by resolving subPath relative to the kibana repo */ + + + getAbsolute(...subPath) { + return path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(this.kibanaProject.path, ...subPath); + } + /** convert an absolute path to a relative path, relative to the kibana repo */ + + + getRelative(absolute) { + return path__WEBPACK_IMPORTED_MODULE_0___default.a.relative(this.kibanaProject.path, absolute); + } + /** get a copy of the map of all projects in the kibana workspace */ + + + getAllProjects() { + return new Map(this.allWorkspaceProjects); + } + /** determine if a project with the given name exists */ + + + hasProject(name) { + return this.allWorkspaceProjects.has(name); + } + /** get a specific project, throws if the name is not known (use hasProject() first) */ + + + getProject(name) { + const project = this.allWorkspaceProjects.get(name); + + if (!project) { + throw new Error(`No package with name "${name}" in the workspace`); + } + + return project; + } + /** get a project and all of the projects it depends on in a ProjectMap */ + + + getProjectAndDeps(name) { + const project = this.getProject(name); + return Object(_projects__WEBPACK_IMPORTED_MODULE_5__["includeTransitiveProjects"])([project], this.allWorkspaceProjects); + } + /** filter the projects to just those matching certain paths/include/exclude tags */ + + + getFilteredProjects(options) { + const allProjects = this.getAllProjects(); + const filteredProjects = new Map(); + const pkgJsonPaths = Array.from(allProjects.values()).map(p => p.packageJsonLocation); + const filteredPkgJsonGlobs = Object(_config__WEBPACK_IMPORTED_MODULE_6__["getProjectPaths"])(_objectSpread(_objectSpread({}, options), {}, { + rootPath: this.kibanaProject.path + })).map(g => path__WEBPACK_IMPORTED_MODULE_0___default.a.resolve(g, 'package.json')); + const matchingPkgJsonPaths = multimatch__WEBPACK_IMPORTED_MODULE_2___default()(pkgJsonPaths, filteredPkgJsonGlobs); + + for (const project of allProjects.values()) { + const pathMatches = matchingPkgJsonPaths.includes(project.packageJsonLocation); + const notExcluded = !options.exclude.includes(project.name); + const isIncluded = !options.include.length || options.include.includes(project.name); + + if (pathMatches && notExcluded && isIncluded) { + filteredProjects.set(project.name, project); + } + } + + return filteredProjects; + } + + isPartOfRepo(project) { + return project.path === this.kibanaProject.path || is_path_inside__WEBPACK_IMPORTED_MODULE_3___default()(project.path, this.kibanaProject.path); + } + + isOutsideRepo(project) { + return !this.isPartOfRepo(project); + } + + resolveAllProductionDependencies(yarnLock, log) { + const kibanaDeps = Object(_yarn_lock__WEBPACK_IMPORTED_MODULE_4__["resolveDepsForProject"])({ + project: this.kibanaProject, + yarnLock, + kbn: this, + includeDependentProject: true, + productionDepsOnly: true, + log + }); + const xpackDeps = Object(_yarn_lock__WEBPACK_IMPORTED_MODULE_4__["resolveDepsForProject"])({ + project: this.getProject('x-pack'), + yarnLock, + kbn: this, + includeDependentProject: true, + productionDepsOnly: true, + log + }); + return new Map([...kibanaDeps.entries(), ...xpackDeps.entries()]); + } + + getUuid() { + try { + return fs__WEBPACK_IMPORTED_MODULE_1___default.a.readFileSync(this.getAbsolute('data/uuid'), 'utf-8').trim(); + } catch (error) { + if (error.code === 'ENOENT') { + return undefined; + } + + throw error; + } + } + +} + +/***/ }), +/* 559 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const minimatch = __webpack_require__(150); +const arrayUnion = __webpack_require__(145); +const arrayDiffer = __webpack_require__(560); +const arrify = __webpack_require__(561); + +module.exports = (list, patterns, options = {}) => { + list = arrify(list); + patterns = arrify(patterns); + + if (list.length === 0 || patterns.length === 0) { + return []; + } + + return patterns.reduce((result, pattern) => { + let process = arrayUnion; + + if (pattern[0] === '!') { + pattern = pattern.slice(1); + process = arrayDiffer; + } + + return process(result, minimatch.match(list, pattern, options)); + }, []); +}; + + +/***/ }), +/* 560 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const arrayDiffer = (array, ...values) => { + const rest = new Set([].concat(...values)); + return array.filter(element => !rest.has(element)); +}; + +module.exports = arrayDiffer; + + +/***/ }), +/* 561 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const arrify = value => { + if (value === null || value === undefined) { + return []; + } + + if (Array.isArray(value)) { + return value; + } + + if (typeof value === 'string') { + return [value]; + } + + if (typeof value[Symbol.iterator] === 'function') { + return [...value]; + } + + return [value]; +}; + +module.exports = arrify; + + +/***/ }), +/* 562 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return getProjectPaths; }); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_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. + */ + + +/** + * Returns all the paths where plugins are located + */ +function getProjectPaths({ + rootPath, + ossOnly, + skipKibanaPlugins +}) { + const projectPaths = [rootPath, Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'packages/*')]; // This is needed in order to install the dependencies for the declared + // plugin functional used in the selenium functional tests. + // As we are now using the webpack dll for the client vendors dependencies + // when we run the plugin functional tests against the distributable + // dependencies used by such plugins like @eui, react and react-dom can't + // be loaded from the dll as the context is different from the one declared + // into the webpack dll reference plugin. + // In anyway, have a plugin declaring their own dependencies is the + // correct and the expect behavior. + + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*')); + + if (!ossOnly) { + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/legacy/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack/test/functional_with_es_ssl/fixtures/plugins/*')); + } + + if (!skipKibanaPlugins) { + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, '../kibana-extra/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, '../kibana-extra/*/packages/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, '../kibana-extra/*/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'plugins/*/packages/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'plugins/*/plugins/*')); + } + + return projectPaths; +} + +/***/ }), +/* 563 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); + +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(783); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); + +/* + * 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. + */ + + + +/***/ }), +/* 564 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(774); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(783); +/* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); +/* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(131); +/* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(251); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(248); +/* + * 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. + */ + + + + + + + + + +async function buildBazelProductionProjects({ + kibanaRoot, + buildRoot, + onlyOSS +}) { + const projects = await Object(_utils_projects__WEBPACK_IMPORTED_MODULE_8__["getBazelProjectsOnly"])(await Object(_build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__["getProductionProjects"])(kibanaRoot, onlyOSS)); + const projectNames = [...projects.values()].map(project => project.name); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`Preparing Bazel projects production build for [${projectNames.join(', ')}]`); + await Object(_utils_bazel__WEBPACK_IMPORTED_MODULE_4__["runBazel"])(['build', '//packages:build']); + _utils_log__WEBPACK_IMPORTED_MODULE_6__["log"].info(`All Bazel projects production builds for [${projectNames.join(', ')}] are complete}]`); + + for (const project of projects.values()) { + await copyToBuild(project, kibanaRoot, buildRoot); + await applyCorrectPermissions(project, kibanaRoot, buildRoot); + } +} +/** + * Copy all the project's files from its Bazel dist directory into the + * project build folder. + * + * When copying all the files into the build, we exclude `node_modules` because + * we want the Kibana build to be responsible for actually installing all + * dependencies. The primary reason for allowing the Kibana build process to + * manage dependencies is that it will "dedupe" them, so we don't include + * unnecessary copies of dependencies. We also exclude every related Bazel build + * files in order to get the most cleaner package module we can in the final distributable. + */ + +async function copyToBuild(project, kibanaRoot, buildRoot) { + // We want the package to have the same relative location within the build + const relativeProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["relative"])(kibanaRoot, project.path); + const buildProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, relativeProjectPath); + await cpy__WEBPACK_IMPORTED_MODULE_0___default()(['**/*'], buildProjectPath, { + cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel', 'bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), + dot: true, + onlyFiles: true, + parents: true + }); // If a project is using an intermediate build directory, we special-case our + // handling of `package.json`, as the project build process might have copied + // (a potentially modified) `package.json` into the intermediate build + // directory already. If so, we want to use that `package.json` as the basis + // for creating the production-ready `package.json`. If it's not present in + // the intermediate build, we fall back to using the project's already defined + // `package.json`. + + const packageJson = (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(buildProjectPath, 'package.json'))) ? await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["readPackageJson"])(buildProjectPath) : project.json; + const preparedPackageJson = Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["createProductionPackageJson"])(packageJson); + await Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_7__["writePackageJson"])(buildProjectPath, preparedPackageJson); +} + +async function applyCorrectPermissions(project, kibanaRoot, buildRoot) { + const relativeProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["relative"])(kibanaRoot, project.path); + const buildProjectPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, relativeProjectPath); + const allPluginPaths = await globby__WEBPACK_IMPORTED_MODULE_1___default()([`**/*`], { + onlyFiles: false, + cwd: Object(path__WEBPACK_IMPORTED_MODULE_2__["join"])(kibanaRoot, 'bazel', 'bin', 'packages', Object(path__WEBPACK_IMPORTED_MODULE_2__["basename"])(buildProjectPath), 'npm_module'), + dot: true + }); + + for (const pluginPath of allPluginPaths) { + const resolvedPluginPath = Object(path__WEBPACK_IMPORTED_MODULE_2__["resolve"])(buildRoot, pluginPath); + + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isFile"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o644); + } + + if (await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["isDirectory"])(resolvedPluginPath)) { + await Object(_utils_fs__WEBPACK_IMPORTED_MODULE_5__["chmod"])(resolvedPluginPath, 0o755); + } + } +} + +/***/ }), +/* 565 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const EventEmitter = __webpack_require__(156); +const path = __webpack_require__(4); +const os = __webpack_require__(121); +const pMap = __webpack_require__(566); +const arrify = __webpack_require__(561); +const globby = __webpack_require__(569); +const hasGlob = __webpack_require__(758); +const cpFile = __webpack_require__(760); +const junk = __webpack_require__(770); +const pFilter = __webpack_require__(771); +const CpyError = __webpack_require__(773); + +const defaultOptions = { + ignoreJunk: true +}; + +class SourceFile { + constructor(relativePath, path) { + this.path = path; + this.relativePath = relativePath; + Object.freeze(this); + } + + get name() { + return path.basename(this.relativePath); + } + + get nameWithoutExtension() { + return path.basename(this.relativePath, path.extname(this.relativePath)); + } + + get extension() { + return path.extname(this.relativePath).slice(1); + } +} + +const preprocessSourcePath = (source, options) => path.resolve(options.cwd ? options.cwd : process.cwd(), source); + +const preprocessDestinationPath = (source, destination, options) => { + let basename = path.basename(source); + + if (typeof options.rename === 'string') { + basename = options.rename; + } else if (typeof options.rename === 'function') { + basename = options.rename(basename); + } + + if (options.cwd) { + destination = path.resolve(options.cwd, destination); + } + + if (options.parents) { + const dirname = path.dirname(source); + const parsedDirectory = path.parse(dirname); + return path.join(destination, dirname.replace(parsedDirectory.root, path.sep), basename); + } + + return path.join(destination, basename); +}; + +module.exports = (source, destination, { + concurrency = (os.cpus().length || 1) * 2, + ...options +} = {}) => { + const progressEmitter = new EventEmitter(); + + options = { + ...defaultOptions, + ...options + }; + + const promise = (async () => { + source = arrify(source); + + if (source.length === 0 || !destination) { + throw new CpyError('`source` and `destination` required'); + } + + const copyStatus = new Map(); + let completedFiles = 0; + let completedSize = 0; + + let files; + try { + files = await globby(source, options); + + if (options.ignoreJunk) { + files = files.filter(file => junk.not(path.basename(file))); + } + } catch (error) { + throw new CpyError(`Cannot glob \`${source}\`: ${error.message}`, error); + } + + if (files.length === 0 && !hasGlob(source)) { + throw new CpyError(`Cannot copy \`${source}\`: the file doesn't exist`); + } + + let sources = files.map(sourcePath => new SourceFile(sourcePath, preprocessSourcePath(sourcePath, options))); + + if (options.filter !== undefined) { + const filteredSources = await pFilter(sources, options.filter, {concurrency: 1024}); + sources = filteredSources; + } + + if (sources.length === 0) { + progressEmitter.emit('progress', { + totalFiles: 0, + percent: 1, + completedFiles: 0, + completedSize: 0 + }); + } + + const fileProgressHandler = event => { + const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0}; + + if (fileStatus.written !== event.written || fileStatus.percent !== event.percent) { + completedSize -= fileStatus.written; + completedSize += event.written; + + if (event.percent === 1 && fileStatus.percent !== 1) { + completedFiles++; + } + + copyStatus.set(event.src, { + written: event.written, + percent: event.percent + }); + + progressEmitter.emit('progress', { + totalFiles: files.length, + percent: completedFiles / files.length, + completedFiles, + completedSize + }); + } + }; + + return pMap(sources, async source => { + const to = preprocessDestinationPath(source.relativePath, destination, options); + + try { + await cpFile(source.path, to, options).on('progress', fileProgressHandler); + } catch (error) { + throw new CpyError(`Cannot copy from \`${source.relativePath}\` to \`${to}\`: ${error.message}`, error); + } + + return to; + }, {concurrency}); + })(); + + promise.on = (...arguments_) => { + progressEmitter.on(...arguments_); + return promise; + }; + + return promise; +}; + + +/***/ }), +/* 566 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const AggregateError = __webpack_require__(567); + +module.exports = async ( + iterable, + mapper, + { + concurrency = Infinity, + stopOnError = true + } = {} +) => { + return new Promise((resolve, reject) => { + if (typeof mapper !== 'function') { + throw new TypeError('Mapper function is required'); + } + + if (!(typeof concurrency === 'number' && concurrency >= 1)) { + throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${concurrency}\` (${typeof concurrency})`); + } + + const ret = []; + const errors = []; + const iterator = iterable[Symbol.iterator](); + let isRejected = false; + let isIterableDone = false; + let resolvingCount = 0; + let currentIndex = 0; + + const next = () => { + if (isRejected) { + return; + } + + const nextItem = iterator.next(); + const i = currentIndex; + currentIndex++; + + if (nextItem.done) { + isIterableDone = true; + + if (resolvingCount === 0) { + if (!stopOnError && errors.length !== 0) { + reject(new AggregateError(errors)); + } else { + resolve(ret); + } + } + + return; + } + + resolvingCount++; + + (async () => { + try { + const element = await nextItem.value; + ret[i] = await mapper(element, i); + resolvingCount--; + next(); + } catch (error) { + if (stopOnError) { + isRejected = true; + reject(error); + } else { + errors.push(error); + resolvingCount--; + next(); + } + } + })(); + }; + + for (let i = 0; i < concurrency; i++) { + next(); + + if (isIterableDone) { + break; + } + } + }); +}; + + +/***/ }), +/* 567 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const indentString = __webpack_require__(568); +const cleanStack = __webpack_require__(244); + +const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); + +class AggregateError extends Error { + constructor(errors) { + if (!Array.isArray(errors)) { + throw new TypeError(`Expected input to be an Array, got ${typeof errors}`); + } + + errors = [...errors].map(error => { + if (error instanceof Error) { + return error; + } + + if (error !== null && typeof error === 'object') { + // Handle plain error objects with message property and/or possibly other metadata + return Object.assign(new Error(error.message), error); + } + + return new Error(error); + }); + + let message = errors + .map(error => { + // The `stack` property is not standardized, so we can't assume it exists + return typeof error.stack === 'string' ? cleanInternalStack(cleanStack(error.stack)) : String(error); + }) + .join('\n'); + message = '\n' + indentString(message, 4); + super(message); + + this.name = 'AggregateError'; + + Object.defineProperty(this, '_errors', {value: errors}); + } + + * [Symbol.iterator]() { + for (const error of this._errors) { + yield error; + } + } +} + +module.exports = AggregateError; + + +/***/ }), +/* 568 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = (string, count = 1, options) => { + options = { + indent: ' ', + includeEmptyLines: false, + ...options + }; + + if (typeof string !== 'string') { + throw new TypeError( + `Expected \`input\` to be a \`string\`, got \`${typeof string}\`` + ); + } + + if (typeof count !== 'number') { + throw new TypeError( + `Expected \`count\` to be a \`number\`, got \`${typeof count}\`` + ); + } + + if (typeof options.indent !== 'string') { + throw new TypeError( + `Expected \`options.indent\` to be a \`string\`, got \`${typeof options.indent}\`` + ); + } + + if (count === 0) { + return string; + } + + const regex = options.includeEmptyLines ? /^/gm : /^(?!\s*$)/gm; + + return string.replace(regex, options.indent.repeat(count)); +}; + + +/***/ }), +/* 569 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const fs = __webpack_require__(134); +const arrayUnion = __webpack_require__(570); +const glob = __webpack_require__(147); +const fastGlob = __webpack_require__(572); +const dirGlob = __webpack_require__(751); +const gitignore = __webpack_require__(754); + +const DEFAULT_FILTER = () => false; + +const isNegative = pattern => pattern[0] === '!'; + +const assertPatternsInput = patterns => { + if (!patterns.every(x => typeof x === 'string')) { + throw new TypeError('Patterns must be a string or an array of strings'); + } +}; + +const checkCwdOption = options => { + if (options && options.cwd && !fs.statSync(options.cwd).isDirectory()) { + throw new Error('The `cwd` option must be a path to a directory'); + } +}; + +const generateGlobTasks = (patterns, taskOptions) => { + patterns = arrayUnion([].concat(patterns)); + assertPatternsInput(patterns); + checkCwdOption(taskOptions); + + const globTasks = []; + + taskOptions = Object.assign({ + ignore: [], + expandDirectories: true + }, taskOptions); + + patterns.forEach((pattern, i) => { + if (isNegative(pattern)) { + return; + } + + const ignore = patterns + .slice(i) + .filter(isNegative) + .map(pattern => pattern.slice(1)); + + const options = Object.assign({}, taskOptions, { + ignore: taskOptions.ignore.concat(ignore) + }); + + globTasks.push({pattern, options}); + }); + + return globTasks; +}; + +const globDirs = (task, fn) => { + let options = {}; + if (task.options.cwd) { + options.cwd = task.options.cwd; + } + + if (Array.isArray(task.options.expandDirectories)) { + options = Object.assign(options, {files: task.options.expandDirectories}); + } else if (typeof task.options.expandDirectories === 'object') { + options = Object.assign(options, task.options.expandDirectories); + } + + return fn(task.pattern, options); +}; + +const getPattern = (task, fn) => task.options.expandDirectories ? globDirs(task, fn) : [task.pattern]; const globToTask = task => glob => { const {options} = task; @@ -60500,12 +64217,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 527 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(528); +var arrayUniq = __webpack_require__(571); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -60513,7 +64230,7 @@ module.exports = function () { /***/ }), -/* 528 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60582,10 +64299,10 @@ if ('Set' in global) { /***/ }), -/* 529 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(530); +const pkg = __webpack_require__(573); module.exports = pkg.async; module.exports.default = pkg.async; @@ -60598,19 +64315,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 530 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(531); -var taskManager = __webpack_require__(532); -var reader_async_1 = __webpack_require__(685); -var reader_stream_1 = __webpack_require__(709); -var reader_sync_1 = __webpack_require__(710); -var arrayUtils = __webpack_require__(712); -var streamUtils = __webpack_require__(713); +var optionsManager = __webpack_require__(574); +var taskManager = __webpack_require__(575); +var reader_async_1 = __webpack_require__(722); +var reader_stream_1 = __webpack_require__(746); +var reader_sync_1 = __webpack_require__(747); +var arrayUtils = __webpack_require__(749); +var streamUtils = __webpack_require__(750); /** * Synchronous API. */ @@ -60676,7 +64393,7 @@ function isString(source) { /***/ }), -/* 531 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60714,13 +64431,13 @@ exports.prepare = prepare; /***/ }), -/* 532 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(533); +var patternUtils = __webpack_require__(576); /** * Generate tasks based on parent directory of each pattern. */ @@ -60811,16 +64528,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 533 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(534); +var globParent = __webpack_require__(577); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(537); +var micromatch = __webpack_require__(580); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -60966,15 +64683,15 @@ exports.matchAny = matchAny; /***/ }), -/* 534 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(535); -var pathDirname = __webpack_require__(536); +var isglob = __webpack_require__(578); +var pathDirname = __webpack_require__(579); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -60997,7 +64714,7 @@ module.exports = function globParent(str) { /***/ }), -/* 535 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -61028,7 +64745,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 536 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61178,7 +64895,7 @@ module.exports.win32 = win32; /***/ }), -/* 537 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61189,18 +64906,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(538); -var toRegex = __webpack_require__(539); -var extend = __webpack_require__(653); +var braces = __webpack_require__(581); +var toRegex = __webpack_require__(582); +var extend = __webpack_require__(690); /** * Local dependencies */ -var compilers = __webpack_require__(655); -var parsers = __webpack_require__(681); -var cache = __webpack_require__(682); -var utils = __webpack_require__(683); +var compilers = __webpack_require__(692); +var parsers = __webpack_require__(718); +var cache = __webpack_require__(719); +var utils = __webpack_require__(720); var MAX_LENGTH = 1024 * 64; /** @@ -62062,7 +65779,7 @@ module.exports = micromatch; /***/ }), -/* 538 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62072,18 +65789,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(539); -var unique = __webpack_require__(559); -var extend = __webpack_require__(560); +var toRegex = __webpack_require__(582); +var unique = __webpack_require__(602); +var extend = __webpack_require__(603); /** * Local dependencies */ -var compilers = __webpack_require__(562); -var parsers = __webpack_require__(575); -var Braces = __webpack_require__(580); -var utils = __webpack_require__(563); +var compilers = __webpack_require__(605); +var parsers = __webpack_require__(618); +var Braces = __webpack_require__(623); +var utils = __webpack_require__(606); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -62387,16 +66104,16 @@ module.exports = braces; /***/ }), -/* 539 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(540); -var define = __webpack_require__(546); -var extend = __webpack_require__(552); -var not = __webpack_require__(556); +var safe = __webpack_require__(583); +var define = __webpack_require__(589); +var extend = __webpack_require__(595); +var not = __webpack_require__(599); var MAX_LENGTH = 1024 * 64; /** @@ -62549,10 +66266,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 540 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(541); +var parse = __webpack_require__(584); var types = parse.types; module.exports = function (re, opts) { @@ -62598,13 +66315,13 @@ function isRegExp (x) { /***/ }), -/* 541 */ +/* 584 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(542); -var types = __webpack_require__(543); -var sets = __webpack_require__(544); -var positions = __webpack_require__(545); +var util = __webpack_require__(585); +var types = __webpack_require__(586); +var sets = __webpack_require__(587); +var positions = __webpack_require__(588); module.exports = function(regexpStr) { @@ -62886,11 +66603,11 @@ module.exports.types = types; /***/ }), -/* 542 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(543); -var sets = __webpack_require__(544); +var types = __webpack_require__(586); +var sets = __webpack_require__(587); // All of these are private and only used by randexp. @@ -63003,7 +66720,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 543 */ +/* 586 */ /***/ (function(module, exports) { module.exports = { @@ -63019,10 +66736,10 @@ module.exports = { /***/ }), -/* 544 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(543); +var types = __webpack_require__(586); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -63107,10 +66824,10 @@ exports.anyChar = function() { /***/ }), -/* 545 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(543); +var types = __webpack_require__(586); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -63130,7 +66847,7 @@ exports.end = function() { /***/ }), -/* 546 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63143,8 +66860,8 @@ exports.end = function() { -var isobject = __webpack_require__(547); -var isDescriptor = __webpack_require__(548); +var isobject = __webpack_require__(590); +var isDescriptor = __webpack_require__(591); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -63175,7 +66892,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 547 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63194,7 +66911,7 @@ module.exports = function isObject(val) { /***/ }), -/* 548 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63207,9 +66924,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(549); -var isAccessor = __webpack_require__(550); -var isData = __webpack_require__(551); +var typeOf = __webpack_require__(592); +var isAccessor = __webpack_require__(593); +var isData = __webpack_require__(594); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -63223,7 +66940,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 549 */ +/* 592 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63358,7 +67075,7 @@ function isBuffer(val) { /***/ }), -/* 550 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63371,7 +67088,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(549); +var typeOf = __webpack_require__(592); // accessor descriptor properties var accessor = { @@ -63434,7 +67151,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 551 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63447,7 +67164,7 @@ module.exports = isAccessorDescriptor; -var typeOf = __webpack_require__(549); +var typeOf = __webpack_require__(592); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -63490,14 +67207,14 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 552 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(553); -var assignSymbols = __webpack_require__(555); +var isExtendable = __webpack_require__(596); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63557,7 +67274,7 @@ function isEnum(obj, key) { /***/ }), -/* 553 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63570,7 +67287,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(554); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63578,7 +67295,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 554 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63591,7 +67308,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(547); +var isObject = __webpack_require__(590); function isObjectObject(o) { return isObject(o) === true @@ -63622,7 +67339,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 555 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63669,14 +67386,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 556 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(557); -var safe = __webpack_require__(540); +var extend = __webpack_require__(600); +var safe = __webpack_require__(583); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -63748,14 +67465,14 @@ module.exports = toRegex; /***/ }), -/* 557 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(558); -var assignSymbols = __webpack_require__(555); +var isExtendable = __webpack_require__(601); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63815,7 +67532,7 @@ function isEnum(obj, key) { /***/ }), -/* 558 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63828,7 +67545,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(554); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63836,7 +67553,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 559 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63886,13 +67603,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 560 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(561); +var isObject = __webpack_require__(604); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -63926,7 +67643,7 @@ function hasOwn(obj, key) { /***/ }), -/* 561 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63946,13 +67663,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 562 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(563); +var utils = __webpack_require__(606); module.exports = function(braces, options) { braces.compiler @@ -64235,25 +67952,25 @@ function hasQueue(node) { /***/ }), -/* 563 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(564); +var splitString = __webpack_require__(607); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(560); -utils.flatten = __webpack_require__(567); -utils.isObject = __webpack_require__(547); -utils.fillRange = __webpack_require__(568); -utils.repeat = __webpack_require__(574); -utils.unique = __webpack_require__(559); +utils.extend = __webpack_require__(603); +utils.flatten = __webpack_require__(610); +utils.isObject = __webpack_require__(590); +utils.fillRange = __webpack_require__(611); +utils.repeat = __webpack_require__(617); +utils.unique = __webpack_require__(602); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -64585,7 +68302,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 564 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64598,7 +68315,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(565); +var extend = __webpack_require__(608); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -64763,14 +68480,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 565 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(566); -var assignSymbols = __webpack_require__(555); +var isExtendable = __webpack_require__(609); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -64830,7 +68547,7 @@ function isEnum(obj, key) { /***/ }), -/* 566 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64843,7 +68560,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(554); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -64851,7 +68568,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 567 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64880,7 +68597,7 @@ function flat(arr, res) { /***/ }), -/* 568 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64894,10 +68611,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(569); -var extend = __webpack_require__(560); -var repeat = __webpack_require__(572); -var toRegex = __webpack_require__(573); +var isNumber = __webpack_require__(612); +var extend = __webpack_require__(603); +var repeat = __webpack_require__(615); +var toRegex = __webpack_require__(616); /** * Return a range of numbers or letters. @@ -65095,7 +68812,7 @@ module.exports = fillRange; /***/ }), -/* 569 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65108,7 +68825,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(570); +var typeOf = __webpack_require__(613); module.exports = function isNumber(num) { var type = typeOf(num); @@ -65124,10 +68841,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 570 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(571); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -65246,7 +68963,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 571 */ +/* 614 */ /***/ (function(module, exports) { /*! @@ -65273,7 +68990,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 572 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65350,7 +69067,7 @@ function repeat(str, num) { /***/ }), -/* 573 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65363,8 +69080,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(572); -var isNumber = __webpack_require__(569); +var repeat = __webpack_require__(615); +var isNumber = __webpack_require__(612); var cache = {}; function toRegexRange(min, max, options) { @@ -65651,7 +69368,7 @@ module.exports = toRegexRange; /***/ }), -/* 574 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65676,14 +69393,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 575 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(576); -var utils = __webpack_require__(563); +var Node = __webpack_require__(619); +var utils = __webpack_require__(606); /** * Braces parsers @@ -66043,15 +69760,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 576 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(547); -var define = __webpack_require__(577); -var utils = __webpack_require__(578); +var isObject = __webpack_require__(590); +var define = __webpack_require__(620); +var utils = __webpack_require__(621); var ownNames; /** @@ -66542,7 +70259,7 @@ exports = module.exports = Node; /***/ }), -/* 577 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66555,7 +70272,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(548); +var isDescriptor = __webpack_require__(591); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66580,13 +70297,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 578 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(579); +var typeOf = __webpack_require__(622); var utils = module.exports; /** @@ -67606,10 +71323,10 @@ function assert(val, message) { /***/ }), -/* 579 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(571); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -67728,17 +71445,17 @@ module.exports = function kindOf(val) { /***/ }), -/* 580 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(560); -var Snapdragon = __webpack_require__(581); -var compilers = __webpack_require__(562); -var parsers = __webpack_require__(575); -var utils = __webpack_require__(563); +var extend = __webpack_require__(603); +var Snapdragon = __webpack_require__(624); +var compilers = __webpack_require__(605); +var parsers = __webpack_require__(618); +var utils = __webpack_require__(606); /** * Customize Snapdragon parser and renderer @@ -67839,17 +71556,17 @@ module.exports = Braces; /***/ }), -/* 581 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(582); -var define = __webpack_require__(610); -var Compiler = __webpack_require__(621); -var Parser = __webpack_require__(650); -var utils = __webpack_require__(630); +var Base = __webpack_require__(625); +var define = __webpack_require__(653); +var Compiler = __webpack_require__(664); +var Parser = __webpack_require__(687); +var utils = __webpack_require__(667); var regexCache = {}; var cache = {}; @@ -68020,20 +71737,20 @@ module.exports.Parser = Parser; /***/ }), -/* 582 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(583); -var CacheBase = __webpack_require__(584); -var Emitter = __webpack_require__(585); -var isObject = __webpack_require__(547); -var merge = __webpack_require__(604); -var pascal = __webpack_require__(607); -var cu = __webpack_require__(608); +var define = __webpack_require__(626); +var CacheBase = __webpack_require__(627); +var Emitter = __webpack_require__(628); +var isObject = __webpack_require__(590); +var merge = __webpack_require__(647); +var pascal = __webpack_require__(650); +var cu = __webpack_require__(651); /** * Optionally define a custom `cache` namespace to use. @@ -68462,7 +72179,7 @@ module.exports.namespace = namespace; /***/ }), -/* 583 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68475,7 +72192,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(548); +var isDescriptor = __webpack_require__(591); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -68500,21 +72217,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 584 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(547); -var Emitter = __webpack_require__(585); -var visit = __webpack_require__(586); -var toPath = __webpack_require__(589); -var union = __webpack_require__(591); -var del = __webpack_require__(595); -var get = __webpack_require__(593); -var has = __webpack_require__(600); -var set = __webpack_require__(603); +var isObject = __webpack_require__(590); +var Emitter = __webpack_require__(628); +var visit = __webpack_require__(629); +var toPath = __webpack_require__(632); +var union = __webpack_require__(634); +var del = __webpack_require__(638); +var get = __webpack_require__(636); +var has = __webpack_require__(643); +var set = __webpack_require__(646); /** * Create a `Cache` constructor that when instantiated will @@ -68768,7 +72485,7 @@ module.exports.namespace = namespace; /***/ }), -/* 585 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { @@ -68937,7 +72654,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 586 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68950,8 +72667,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(587); -var mapVisit = __webpack_require__(588); +var visit = __webpack_require__(630); +var mapVisit = __webpack_require__(631); module.exports = function(collection, method, val) { var result; @@ -68974,7 +72691,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 587 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68987,7 +72704,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(547); +var isObject = __webpack_require__(590); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -69014,14 +72731,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 588 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(587); +var visit = __webpack_require__(630); /** * Map `visit` over an array of objects. @@ -69058,7 +72775,7 @@ function isObject(val) { /***/ }), -/* 589 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69071,7 +72788,7 @@ function isObject(val) { -var typeOf = __webpack_require__(590); +var typeOf = __webpack_require__(633); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -69098,10 +72815,10 @@ function filter(arr) { /***/ }), -/* 590 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(571); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -69220,16 +72937,16 @@ module.exports = function kindOf(val) { /***/ }), -/* 591 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(561); -var union = __webpack_require__(592); -var get = __webpack_require__(593); -var set = __webpack_require__(594); +var isObject = __webpack_require__(604); +var union = __webpack_require__(635); +var get = __webpack_require__(636); +var set = __webpack_require__(637); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -69257,7 +72974,7 @@ function arrayify(val) { /***/ }), -/* 592 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69293,7 +73010,7 @@ module.exports = function union(init) { /***/ }), -/* 593 */ +/* 636 */ /***/ (function(module, exports) { /*! @@ -69349,7 +73066,7 @@ function toString(val) { /***/ }), -/* 594 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69362,10 +73079,10 @@ function toString(val) { -var split = __webpack_require__(564); -var extend = __webpack_require__(560); -var isPlainObject = __webpack_require__(554); -var isObject = __webpack_require__(561); +var split = __webpack_require__(607); +var extend = __webpack_require__(603); +var isPlainObject = __webpack_require__(597); +var isObject = __webpack_require__(604); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69411,7 +73128,7 @@ function isValidKey(key) { /***/ }), -/* 595 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69424,8 +73141,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(547); -var has = __webpack_require__(596); +var isObject = __webpack_require__(590); +var has = __webpack_require__(639); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -69450,7 +73167,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 596 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69463,9 +73180,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(597); -var hasValues = __webpack_require__(599); -var get = __webpack_require__(593); +var isObject = __webpack_require__(640); +var hasValues = __webpack_require__(642); +var get = __webpack_require__(636); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -69476,7 +73193,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 597 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69489,7 +73206,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(598); +var isArray = __webpack_require__(641); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69497,7 +73214,7 @@ module.exports = function isObject(val) { /***/ }), -/* 598 */ +/* 641 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69508,7 +73225,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 599 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69551,7 +73268,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 600 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69564,9 +73281,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(547); -var hasValues = __webpack_require__(601); -var get = __webpack_require__(593); +var isObject = __webpack_require__(590); +var hasValues = __webpack_require__(644); +var get = __webpack_require__(636); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69574,7 +73291,7 @@ module.exports = function(val, prop) { /***/ }), -/* 601 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69587,8 +73304,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(602); -var isNumber = __webpack_require__(569); +var typeOf = __webpack_require__(645); +var isNumber = __webpack_require__(612); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -69641,10 +73358,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 602 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(571); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -69766,7 +73483,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 603 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69779,10 +73496,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(564); -var extend = __webpack_require__(560); -var isPlainObject = __webpack_require__(554); -var isObject = __webpack_require__(561); +var split = __webpack_require__(607); +var extend = __webpack_require__(603); +var isPlainObject = __webpack_require__(597); +var isObject = __webpack_require__(604); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69828,14 +73545,14 @@ function isValidKey(key) { /***/ }), -/* 604 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(605); -var forIn = __webpack_require__(606); +var isExtendable = __webpack_require__(648); +var forIn = __webpack_require__(649); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69899,7 +73616,7 @@ module.exports = mixinDeep; /***/ }), -/* 605 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69912,7 +73629,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(554); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -69920,7 +73637,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 606 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69943,7 +73660,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 607 */ +/* 650 */ /***/ (function(module, exports) { /*! @@ -69970,14 +73687,14 @@ module.exports = pascalcase; /***/ }), -/* 608 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(609); +var utils = __webpack_require__(652); /** * Expose class utils @@ -70342,7 +74059,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 609 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70352,508 +74069,94 @@ var utils = {}; -/** - * Lazily required module dependencies - */ - -utils.union = __webpack_require__(592); -utils.define = __webpack_require__(610); -utils.isObj = __webpack_require__(547); -utils.staticExtend = __webpack_require__(617); - - -/** - * Expose `utils` - */ - -module.exports = utils; - - -/***/ }), -/* 610 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * define-property - * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -var isDescriptor = __webpack_require__(611); - -module.exports = function defineProperty(obj, prop, val) { - if (typeof obj !== 'object' && typeof obj !== 'function') { - throw new TypeError('expected an object or function.'); - } - - if (typeof prop !== 'string') { - throw new TypeError('expected `prop` to be a string.'); - } - - if (isDescriptor(val) && ('set' in val || 'get' in val)) { - return Object.defineProperty(obj, prop, val); - } - - return Object.defineProperty(obj, prop, { - configurable: true, - enumerable: false, - writable: true, - value: val - }); -}; - - -/***/ }), -/* 611 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * is-descriptor - * - * Copyright (c) 2015-2017, Jon Schlinkert. - * Released under the MIT License. - */ - - - -var typeOf = __webpack_require__(612); -var isAccessor = __webpack_require__(613); -var isData = __webpack_require__(615); - -module.exports = function isDescriptor(obj, key) { - if (typeOf(obj) !== 'object') { - return false; - } - if ('get' in obj) { - return isAccessor(obj, key); - } - return isData(obj, key); -}; - - -/***/ }), -/* 612 */ -/***/ (function(module, exports) { - -var toString = Object.prototype.toString; - -/** - * Get the native `typeof` a value. - * - * @param {*} `val` - * @return {*} Native javascript type - */ - -module.exports = function kindOf(val) { - var type = typeof val; - - // primitivies - if (type === 'undefined') { - return 'undefined'; - } - if (val === null) { - return 'null'; - } - if (val === true || val === false || val instanceof Boolean) { - return 'boolean'; - } - if (type === 'string' || val instanceof String) { - return 'string'; - } - if (type === 'number' || val instanceof Number) { - return 'number'; - } - - // functions - if (type === 'function' || val instanceof Function) { - if (typeof val.constructor.name !== 'undefined' && val.constructor.name.slice(0, 9) === 'Generator') { - return 'generatorfunction'; - } - return 'function'; - } - - // array - if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { - return 'array'; - } - - // check for instances of RegExp and Date before calling `toString` - if (val instanceof RegExp) { - return 'regexp'; - } - if (val instanceof Date) { - return 'date'; - } - - // other objects - type = toString.call(val); - - if (type === '[object RegExp]') { - return 'regexp'; - } - if (type === '[object Date]') { - return 'date'; - } - if (type === '[object Arguments]') { - return 'arguments'; - } - if (type === '[object Error]') { - return 'error'; - } - if (type === '[object Promise]') { - return 'promise'; - } - - // buffer - if (isBuffer(val)) { - return 'buffer'; - } - - // es6: Map, WeakMap, Set, WeakSet - if (type === '[object Set]') { - return 'set'; - } - if (type === '[object WeakSet]') { - return 'weakset'; - } - if (type === '[object Map]') { - return 'map'; - } - if (type === '[object WeakMap]') { - return 'weakmap'; - } - if (type === '[object Symbol]') { - return 'symbol'; - } - - if (type === '[object Map Iterator]') { - return 'mapiterator'; - } - if (type === '[object Set Iterator]') { - return 'setiterator'; - } - if (type === '[object String Iterator]') { - return 'stringiterator'; - } - if (type === '[object Array Iterator]') { - return 'arrayiterator'; - } - - // typed arrays - if (type === '[object Int8Array]') { - return 'int8array'; - } - if (type === '[object Uint8Array]') { - return 'uint8array'; - } - if (type === '[object Uint8ClampedArray]') { - return 'uint8clampedarray'; - } - if (type === '[object Int16Array]') { - return 'int16array'; - } - if (type === '[object Uint16Array]') { - return 'uint16array'; - } - if (type === '[object Int32Array]') { - return 'int32array'; - } - if (type === '[object Uint32Array]') { - return 'uint32array'; - } - if (type === '[object Float32Array]') { - return 'float32array'; - } - if (type === '[object Float64Array]') { - return 'float64array'; - } - - // must be a plain object - return 'object'; -}; - -/** - * If you need to support Safari 5-7 (8-10 yr-old browser), - * take a look at https://github.com/feross/is-buffer - */ - -function isBuffer(val) { - return val.constructor - && typeof val.constructor.isBuffer === 'function' - && val.constructor.isBuffer(val); -} - - -/***/ }), -/* 613 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * is-accessor-descriptor - * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -var typeOf = __webpack_require__(614); - -// accessor descriptor properties -var accessor = { - get: 'function', - set: 'function', - configurable: 'boolean', - enumerable: 'boolean' -}; - -function isAccessorDescriptor(obj, prop) { - if (typeof prop === 'string') { - var val = Object.getOwnPropertyDescriptor(obj, prop); - return typeof val !== 'undefined'; - } - - if (typeOf(obj) !== 'object') { - return false; - } - - if (has(obj, 'value') || has(obj, 'writable')) { - return false; - } - - if (!has(obj, 'get') || typeof obj.get !== 'function') { - return false; - } - - // tldr: it's valid to have "set" be undefined - // "set" might be undefined if `Object.getOwnPropertyDescriptor` - // was used to get the value, and only `get` was defined by the user - if (has(obj, 'set') && typeof obj[key] !== 'function' && typeof obj[key] !== 'undefined') { - return false; - } - - for (var key in obj) { - if (!accessor.hasOwnProperty(key)) { - continue; - } - - if (typeOf(obj[key]) === accessor[key]) { - continue; - } - - if (typeof obj[key] !== 'undefined') { - return false; - } - } - return true; -} +/** + * Lazily required module dependencies + */ + +utils.union = __webpack_require__(635); +utils.define = __webpack_require__(653); +utils.isObj = __webpack_require__(590); +utils.staticExtend = __webpack_require__(660); -function has(obj, key) { - return {}.hasOwnProperty.call(obj, key); -} /** - * Expose `isAccessorDescriptor` + * Expose `utils` */ -module.exports = isAccessorDescriptor; +module.exports = utils; /***/ }), -/* 614 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(571); -var toString = Object.prototype.toString; - -/** - * Get the native `typeof` a value. +"use strict"; +/*! + * define-property * - * @param {*} `val` - * @return {*} Native javascript type + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. */ -module.exports = function kindOf(val) { - // primitivies - if (typeof val === 'undefined') { - return 'undefined'; - } - if (val === null) { - return 'null'; - } - if (val === true || val === false || val instanceof Boolean) { - return 'boolean'; - } - if (typeof val === 'string' || val instanceof String) { - return 'string'; - } - if (typeof val === 'number' || val instanceof Number) { - return 'number'; - } - - // functions - if (typeof val === 'function' || val instanceof Function) { - return 'function'; - } - - // array - if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { - return 'array'; - } - - // check for instances of RegExp and Date before calling `toString` - if (val instanceof RegExp) { - return 'regexp'; - } - if (val instanceof Date) { - return 'date'; - } - // other objects - var type = toString.call(val); - if (type === '[object RegExp]') { - return 'regexp'; - } - if (type === '[object Date]') { - return 'date'; - } - if (type === '[object Arguments]') { - return 'arguments'; - } - if (type === '[object Error]') { - return 'error'; - } +var isDescriptor = __webpack_require__(654); - // buffer - if (isBuffer(val)) { - return 'buffer'; +module.exports = function defineProperty(obj, prop, val) { + if (typeof obj !== 'object' && typeof obj !== 'function') { + throw new TypeError('expected an object or function.'); } - // es6: Map, WeakMap, Set, WeakSet - if (type === '[object Set]') { - return 'set'; - } - if (type === '[object WeakSet]') { - return 'weakset'; - } - if (type === '[object Map]') { - return 'map'; - } - if (type === '[object WeakMap]') { - return 'weakmap'; - } - if (type === '[object Symbol]') { - return 'symbol'; + if (typeof prop !== 'string') { + throw new TypeError('expected `prop` to be a string.'); } - // typed arrays - if (type === '[object Int8Array]') { - return 'int8array'; - } - if (type === '[object Uint8Array]') { - return 'uint8array'; - } - if (type === '[object Uint8ClampedArray]') { - return 'uint8clampedarray'; - } - if (type === '[object Int16Array]') { - return 'int16array'; - } - if (type === '[object Uint16Array]') { - return 'uint16array'; - } - if (type === '[object Int32Array]') { - return 'int32array'; - } - if (type === '[object Uint32Array]') { - return 'uint32array'; - } - if (type === '[object Float32Array]') { - return 'float32array'; - } - if (type === '[object Float64Array]') { - return 'float64array'; + if (isDescriptor(val) && ('set' in val || 'get' in val)) { + return Object.defineProperty(obj, prop, val); } - // must be a plain object - return 'object'; + return Object.defineProperty(obj, prop, { + configurable: true, + enumerable: false, + writable: true, + value: val + }); }; /***/ }), -/* 615 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * is-data-descriptor + * is-descriptor * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. + * Copyright (c) 2015-2017, Jon Schlinkert. + * Released under the MIT License. */ -var typeOf = __webpack_require__(616); - -// data descriptor properties -var data = { - configurable: 'boolean', - enumerable: 'boolean', - writable: 'boolean' -}; +var typeOf = __webpack_require__(655); +var isAccessor = __webpack_require__(656); +var isData = __webpack_require__(658); -function isDataDescriptor(obj, prop) { +module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { return false; } - - if (typeof prop === 'string') { - var val = Object.getOwnPropertyDescriptor(obj, prop); - return typeof val !== 'undefined'; - } - - if (!('value' in obj) && !('writable' in obj)) { - return false; - } - - for (var key in obj) { - if (key === 'value') continue; - - if (!data.hasOwnProperty(key)) { - continue; - } - - if (typeOf(obj[key]) === data[key]) { - continue; - } - - if (typeof obj[key] !== 'undefined') { - return false; - } + if ('get' in obj) { + return isAccessor(obj, key); } - return true; -} - -/** - * Expose `isDataDescriptor` - */ - -module.exports = isDataDescriptor; + return isData(obj, key); +}; /***/ }), -/* 616 */ -/***/ (function(module, exports, __webpack_require__) { +/* 655 */ +/***/ (function(module, exports) { -var isBuffer = __webpack_require__(571); var toString = Object.prototype.toString; /** @@ -70864,8 +74167,10 @@ var toString = Object.prototype.toString; */ module.exports = function kindOf(val) { + var type = typeof val; + // primitivies - if (typeof val === 'undefined') { + if (type === 'undefined') { return 'undefined'; } if (val === null) { @@ -70874,15 +74179,18 @@ module.exports = function kindOf(val) { if (val === true || val === false || val instanceof Boolean) { return 'boolean'; } - if (typeof val === 'string' || val instanceof String) { + if (type === 'string' || val instanceof String) { return 'string'; } - if (typeof val === 'number' || val instanceof Number) { + if (type === 'number' || val instanceof Number) { return 'number'; } // functions - if (typeof val === 'function' || val instanceof Function) { + if (type === 'function' || val instanceof Function) { + if (typeof val.constructor.name !== 'undefined' && val.constructor.name.slice(0, 9) === 'Generator') { + return 'generatorfunction'; + } return 'function'; } @@ -70900,7 +74208,7 @@ module.exports = function kindOf(val) { } // other objects - var type = toString.call(val); + type = toString.call(val); if (type === '[object RegExp]') { return 'regexp'; @@ -70914,6 +74222,9 @@ module.exports = function kindOf(val) { if (type === '[object Error]') { return 'error'; } + if (type === '[object Promise]') { + return 'promise'; + } // buffer if (isBuffer(val)) { @@ -70936,7 +74247,20 @@ module.exports = function kindOf(val) { if (type === '[object Symbol]') { return 'symbol'; } - + + if (type === '[object Map Iterator]') { + return 'mapiterator'; + } + if (type === '[object Set Iterator]') { + return 'setiterator'; + } + if (type === '[object String Iterator]') { + return 'stringiterator'; + } + if (type === '[object Array Iterator]') { + return 'arrayiterator'; + } + // typed arrays if (type === '[object Int8Array]') { return 'int8array'; @@ -70970,290 +74294,99 @@ module.exports = function kindOf(val) { return 'object'; }; - -/***/ }), -/* 617 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * static-extend - * - * Copyright (c) 2016, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -var copy = __webpack_require__(618); -var define = __webpack_require__(610); -var util = __webpack_require__(112); - -/** - * Returns a function for extending the static properties, - * prototype properties, and descriptors from the `Parent` - * constructor onto `Child` constructors. - * - * ```js - * var extend = require('static-extend'); - * Parent.extend = extend(Parent); - * - * // optionally pass a custom merge function as the second arg - * Parent.extend = extend(Parent, function(Child) { - * Child.prototype.mixin = function(key, val) { - * Child.prototype[key] = val; - * }; - * }); - * - * // extend "child" constructors - * Parent.extend(Child); - * - * // optionally define prototype methods as the second arg - * Parent.extend(Child, { - * foo: function() {}, - * bar: function() {} - * }); - * ``` - * @param {Function} `Parent` Parent ctor - * @param {Function} `extendFn` Optional extend function for handling any necessary custom merging. Useful when updating methods that require a specific prototype. - * @param {Function} `Child` Child ctor - * @param {Object} `proto` Optionally pass additional prototype properties to inherit. - * @return {Object} - * @api public - */ - -function extend(Parent, extendFn) { - if (typeof Parent !== 'function') { - throw new TypeError('expected Parent to be a function.'); - } - - return function(Ctor, proto) { - if (typeof Ctor !== 'function') { - throw new TypeError('expected Ctor to be a function.'); - } - - util.inherits(Ctor, Parent); - copy(Ctor, Parent); - - // proto can be null or a plain object - if (typeof proto === 'object') { - var obj = Object.create(proto); - - for (var k in obj) { - Ctor.prototype[k] = obj[k]; - } - } - - // keep a reference to the parent prototype - define(Ctor.prototype, '_parent_', { - configurable: true, - set: function() {}, - get: function() { - return Parent.prototype; - } - }); - - if (typeof extendFn === 'function') { - extendFn(Ctor, Parent); - } - - Ctor.extend = extend(Ctor, extendFn); - }; -}; - /** - * Expose `extend` + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer */ -module.exports = extend; +function isBuffer(val) { + return val.constructor + && typeof val.constructor.isBuffer === 'function' + && val.constructor.isBuffer(val); +} /***/ }), -/* 618 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; - - -var typeOf = __webpack_require__(619); -var copyDescriptor = __webpack_require__(620); -var define = __webpack_require__(610); - -/** - * Copy static properties, prototype properties, and descriptors from one object to another. - * - * ```js - * function App() {} - * var proto = App.prototype; - * App.prototype.set = function() {}; - * App.prototype.get = function() {}; +/*! + * is-accessor-descriptor * - * var obj = {}; - * copy(obj, proto); - * ``` - * @param {Object} `receiver` - * @param {Object} `provider` - * @param {String|Array} `omit` One or more properties to omit - * @return {Object} - * @api public + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. */ -function copy(receiver, provider, omit) { - if (!isObject(receiver)) { - throw new TypeError('expected receiving object to be an object.'); - } - if (!isObject(provider)) { - throw new TypeError('expected providing object to be an object.'); - } - var props = nativeKeys(provider); - var keys = Object.keys(provider); - var len = props.length; - omit = arrayify(omit); - while (len--) { - var key = props[len]; +var typeOf = __webpack_require__(657); - if (has(keys, key)) { - define(receiver, key, provider[key]); - } else if (!(key in receiver) && !has(omit, key)) { - copyDescriptor(receiver, provider, key); - } - } +// accessor descriptor properties +var accessor = { + get: 'function', + set: 'function', + configurable: 'boolean', + enumerable: 'boolean' }; -/** - * Return true if the given value is an object or function - */ - -function isObject(val) { - return typeOf(val) === 'object' || typeof val === 'function'; -} - -/** - * Returns true if an array has any of the given elements, or an - * object has any of the give keys. - * - * ```js - * has(['a', 'b', 'c'], 'c'); - * //=> true - * - * has(['a', 'b', 'c'], ['c', 'z']); - * //=> true - * - * has({a: 'b', c: 'd'}, ['c', 'z']); - * //=> true - * ``` - * @param {Object} `obj` - * @param {String|Array} `val` - * @return {Boolean} - */ - -function has(obj, val) { - val = arrayify(val); - var len = val.length; - - if (isObject(obj)) { - for (var key in obj) { - if (val.indexOf(key) > -1) { - return true; - } - } +function isAccessorDescriptor(obj, prop) { + if (typeof prop === 'string') { + var val = Object.getOwnPropertyDescriptor(obj, prop); + return typeof val !== 'undefined'; + } - var keys = nativeKeys(obj); - return has(keys, val); + if (typeOf(obj) !== 'object') { + return false; } - if (Array.isArray(obj)) { - var arr = obj; - while (len--) { - if (arr.indexOf(val[len]) > -1) { - return true; - } - } + if (has(obj, 'value') || has(obj, 'writable')) { return false; } - throw new TypeError('expected an array or object.'); -} + if (!has(obj, 'get') || typeof obj.get !== 'function') { + return false; + } -/** - * Cast the given value to an array. - * - * ```js - * arrayify('foo'); - * //=> ['foo'] - * - * arrayify(['foo']); - * //=> ['foo'] - * ``` - * - * @param {String|Array} `val` - * @return {Array} - */ + // tldr: it's valid to have "set" be undefined + // "set" might be undefined if `Object.getOwnPropertyDescriptor` + // was used to get the value, and only `get` was defined by the user + if (has(obj, 'set') && typeof obj[key] !== 'function' && typeof obj[key] !== 'undefined') { + return false; + } -function arrayify(val) { - return val ? (Array.isArray(val) ? val : [val]) : []; -} + for (var key in obj) { + if (!accessor.hasOwnProperty(key)) { + continue; + } -/** - * Returns true if a value has a `contructor` - * - * ```js - * hasConstructor({}); - * //=> true - * - * hasConstructor(Object.create(null)); - * //=> false - * ``` - * @param {Object} `value` - * @return {Boolean} - */ + if (typeOf(obj[key]) === accessor[key]) { + continue; + } -function hasConstructor(val) { - return isObject(val) && typeof val.constructor !== 'undefined'; + if (typeof obj[key] !== 'undefined') { + return false; + } + } + return true; } -/** - * Get the native `ownPropertyNames` from the constructor of the - * given `object`. An empty array is returned if the object does - * not have a constructor. - * - * ```js - * nativeKeys({a: 'b', b: 'c', c: 'd'}) - * //=> ['a', 'b', 'c'] - * - * nativeKeys(function(){}) - * //=> ['length', 'caller'] - * ``` - * - * @param {Object} `obj` Object that has a `constructor`. - * @return {Array} Array of keys. - */ - -function nativeKeys(val) { - if (!hasConstructor(val)) return []; - return Object.getOwnPropertyNames(val); +function has(obj, key) { + return {}.hasOwnProperty.call(obj, key); } /** - * Expose `copy` - */ - -module.exports = copy; - -/** - * Expose `copy.has` for tests + * Expose `isAccessorDescriptor` */ -module.exports.has = has; +module.exports = isAccessorDescriptor; /***/ }), -/* 619 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(571); +var isBuffer = __webpack_require__(614); var toString = Object.prototype.toString; /** @@ -71371,1271 +74504,1022 @@ module.exports = function kindOf(val) { }; -/***/ }), -/* 620 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * copy-descriptor - * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -/** - * Copy a descriptor from one object to another. - * - * ```js - * function App() { - * this.cache = {}; - * } - * App.prototype.set = function(key, val) { - * this.cache[key] = val; - * return this; - * }; - * Object.defineProperty(App.prototype, 'count', { - * get: function() { - * return Object.keys(this.cache).length; - * } - * }); - * - * copy(App.prototype, 'count', 'len'); - * - * // create an instance - * var app = new App(); - * - * app.set('a', true); - * app.set('b', true); - * app.set('c', true); - * - * console.log(app.count); - * //=> 3 - * console.log(app.len); - * //=> 3 - * ``` - * @name copy - * @param {Object} `receiver` The target object - * @param {Object} `provider` The provider object - * @param {String} `from` The key to copy on provider. - * @param {String} `to` Optionally specify a new key name to use. - * @return {Object} - * @api public - */ - -module.exports = function copyDescriptor(receiver, provider, from, to) { - if (!isObject(provider) && typeof provider !== 'function') { - to = from; - from = provider; - provider = receiver; - } - if (!isObject(receiver) && typeof receiver !== 'function') { - throw new TypeError('expected the first argument to be an object'); - } - if (!isObject(provider) && typeof provider !== 'function') { - throw new TypeError('expected provider to be an object'); - } - - if (typeof to !== 'string') { - to = from; - } - if (typeof from !== 'string') { - throw new TypeError('expected key to be a string'); - } - - if (!(from in provider)) { - throw new Error('property "' + from + '" does not exist'); - } - - var val = Object.getOwnPropertyDescriptor(provider, from); - if (val) Object.defineProperty(receiver, to, val); -}; - -function isObject(val) { - return {}.toString.call(val) === '[object Object]'; -} - - - -/***/ }), -/* 621 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var use = __webpack_require__(622); -var define = __webpack_require__(610); -var debug = __webpack_require__(624)('snapdragon:compiler'); -var utils = __webpack_require__(630); - -/** - * Create a new `Compiler` with the given `options`. - * @param {Object} `options` - */ - -function Compiler(options, state) { - debug('initializing', __filename); - this.options = utils.extend({source: 'string'}, options); - this.state = state || {}; - this.compilers = {}; - this.output = ''; - this.set('eos', function(node) { - return this.emit(node.val, node); - }); - this.set('noop', function(node) { - return this.emit(node.val, node); - }); - this.set('bos', function(node) { - return this.emit(node.val, node); - }); - use(this); -} - -/** - * Prototype methods - */ - -Compiler.prototype = { - - /** - * Throw an error message with details including the cursor position. - * @param {String} `msg` Message to use in the Error. - */ - - error: function(msg, node) { - var pos = node.position || {start: {column: 0}}; - var message = this.options.source + ' column:' + pos.start.column + ': ' + msg; - - var err = new Error(message); - err.reason = msg; - err.column = pos.start.column; - err.source = this.pattern; - - if (this.options.silent) { - this.errors.push(err); - } else { - throw err; - } - }, - - /** - * Define a non-enumberable property on the `Compiler` instance. - * - * ```js - * compiler.define('foo', 'bar'); - * ``` - * @name .define - * @param {String} `key` propery name - * @param {any} `val` property value - * @return {Object} Returns the Compiler instance for chaining. - * @api public - */ - - define: function(key, val) { - define(this, key, val); - return this; - }, - - /** - * Emit `node.val` - */ - - emit: function(str, node) { - this.output += str; - return str; - }, - - /** - * Add a compiler `fn` with the given `name` - */ - - set: function(name, fn) { - this.compilers[name] = fn; - return this; - }, - - /** - * Get compiler `name`. - */ - - get: function(name) { - return this.compilers[name]; - }, - - /** - * Get the previous AST node. - */ - - prev: function(n) { - return this.ast.nodes[this.idx - (n || 1)] || { type: 'bos', val: '' }; - }, - - /** - * Get the next AST node. - */ - - next: function(n) { - return this.ast.nodes[this.idx + (n || 1)] || { type: 'eos', val: '' }; - }, - - /** - * Visit `node`. - */ - - visit: function(node, nodes, i) { - var fn = this.compilers[node.type]; - this.idx = i; - - if (typeof fn !== 'function') { - throw this.error('compiler "' + node.type + '" is not registered', node); - } - return fn.call(this, node, nodes, i); - }, - - /** - * Map visit over array of `nodes`. - */ - - mapVisit: function(nodes) { - if (!Array.isArray(nodes)) { - throw new TypeError('expected an array'); - } - var len = nodes.length; - var idx = -1; - while (++idx < len) { - this.visit(nodes[idx], nodes, idx); - } - return this; - }, - - /** - * Compile `ast`. - */ - - compile: function(ast, options) { - var opts = utils.extend({}, this.options, options); - this.ast = ast; - this.parsingErrors = this.ast.errors; - this.output = ''; - - // source map support - if (opts.sourcemap) { - var sourcemaps = __webpack_require__(649); - sourcemaps(this); - this.mapVisit(this.ast.nodes); - this.applySourceMaps(); - this.map = opts.sourcemap === 'generator' ? this.map : this.map.toJSON(); - return this; - } - - this.mapVisit(this.ast.nodes); - return this; - } -}; - -/** - * Expose `Compiler` - */ - -module.exports = Compiler; - - -/***/ }), -/* 622 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * use - * - * Copyright (c) 2015, 2017, Jon Schlinkert. - * Released under the MIT License. - */ - - - -var utils = __webpack_require__(623); - -module.exports = function base(app, opts) { - if (!utils.isObject(app) && typeof app !== 'function') { - throw new TypeError('use: expect `app` be an object or function'); - } - - if (!utils.isObject(opts)) { - opts = {}; - } - - var prop = utils.isString(opts.prop) ? opts.prop : 'fns'; - if (!Array.isArray(app[prop])) { - utils.define(app, prop, []); - } - - /** - * Define a plugin function to be passed to use. The only - * parameter exposed to the plugin is `app`, the object or function. - * passed to `use(app)`. `app` is also exposed as `this` in plugins. - * - * Additionally, **if a plugin returns a function, the function will - * be pushed onto the `fns` array**, allowing the plugin to be - * called at a later point by the `run` method. - * - * ```js - * var use = require('use'); - * - * // define a plugin - * function foo(app) { - * // do stuff - * } - * - * var app = function(){}; - * use(app); - * - * // register plugins - * app.use(foo); - * app.use(bar); - * app.use(baz); - * ``` - * @name .use - * @param {Function} `fn` plugin function to call - * @api public - */ - - utils.define(app, 'use', use); - - /** - * Run all plugins on `fns`. Any plugin that returns a function - * when called by `use` is pushed onto the `fns` array. - * - * ```js - * var config = {}; - * app.run(config); - * ``` - * @name .run - * @param {Object} `value` Object to be modified by plugins. - * @return {Object} Returns the object passed to `run` - * @api public - */ - - utils.define(app, 'run', function(val) { - if (!utils.isObject(val)) return; - decorate(val); - - var self = this || app; - var fns = self[prop]; - var len = fns.length; - var idx = -1; - - while (++idx < len) { - val.use(fns[idx]); - } - return val; - }); - - /** - * Call plugin `fn`. If a function is returned push it into the - * `fns` array to be called by the `run` method. - */ - - function use(fn, options) { - if (typeof fn !== 'function') { - throw new TypeError('.use expects `fn` be a function'); - } - - var self = this || app; - if (typeof opts.fn === 'function') { - opts.fn.call(self, self, options); - } - - var plugin = fn.call(self, self); - if (typeof plugin === 'function') { - var fns = self[prop]; - fns.push(plugin); - } - return self; - } - - /** - * Ensure the `.use` method exists on `val` - */ - - function decorate(val) { - if (!val.use || !val.run) { - base(val); - } - } - - return app; -}; - - -/***/ }), -/* 623 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var utils = {}; - - - -/** - * Lazily required module dependencies - */ - -utils.define = __webpack_require__(610); -utils.isObject = __webpack_require__(547); - - -utils.isString = function(val) { - return val && typeof val === 'string'; -}; - -/** - * Expose `utils` modules - */ - -module.exports = utils; - - -/***/ }), -/* 624 */ -/***/ (function(module, exports, __webpack_require__) { - -/** - * Detect Electron renderer process, which is node, but we should - * treat as a browser. - */ - -if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(625); -} else { - module.exports = __webpack_require__(628); -} - - -/***/ }), -/* 625 */ -/***/ (function(module, exports, __webpack_require__) { - -/** - * This is the web browser implementation of `debug()`. - * - * Expose `debug()` as the module. - */ - -exports = module.exports = __webpack_require__(626); -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; -exports.storage = 'undefined' != typeof chrome - && 'undefined' != typeof chrome.storage - ? chrome.storage.local - : localstorage(); - -/** - * Colors. - */ - -exports.colors = [ - 'lightseagreen', - 'forestgreen', - 'goldenrod', - 'dodgerblue', - 'darkorchid', - 'crimson' -]; - -/** - * Currently only WebKit-based Web Inspectors, Firefox >= v31, - * and the Firebug extension (any Firefox version) are known - * to support "%c" CSS customizations. - * - * TODO: add a `localStorage` variable to explicitly enable/disable colors - */ - -function useColors() { - // NB: In an Electron preload script, document will be defined but not fully - // initialized. Since we know we're in Chrome, we'll just detect this case - // explicitly - if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { - return true; - } - - // is webkit? http://stackoverflow.com/a/16459606/376773 - // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 - return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || - // is firebug? http://stackoverflow.com/a/398120/376773 - (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || - // is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || - // double check webkit in userAgent just in case we are in a worker - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); -} - -/** - * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. - */ - -exports.formatters.j = function(v) { - try { - return JSON.stringify(v); - } catch (err) { - return '[UnexpectedJSONParseError]: ' + err.message; - } -}; - - -/** - * Colorize log arguments if enabled. +/***/ }), +/* 658 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-data-descriptor * - * @api public + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. */ -function formatArgs(args) { - var useColors = this.useColors; - args[0] = (useColors ? '%c' : '') - + this.namespace - + (useColors ? ' %c' : ' ') - + args[0] - + (useColors ? '%c ' : ' ') - + '+' + exports.humanize(this.diff); - if (!useColors) return; +var typeOf = __webpack_require__(659); - var c = 'color: ' + this.color; - args.splice(1, 0, c, 'color: inherit') +// data descriptor properties +var data = { + configurable: 'boolean', + enumerable: 'boolean', + writable: 'boolean' +}; - // the final "%c" is somewhat tricky, because there could be other - // arguments passed either before or after the %c, so we need to - // figure out the correct index to insert the CSS into - var index = 0; - var lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, function(match) { - if ('%%' === match) return; - index++; - if ('%c' === match) { - // we only are interested in the *last* %c - // (the user may have provided their own) - lastC = index; +function isDataDescriptor(obj, prop) { + if (typeOf(obj) !== 'object') { + return false; + } + + if (typeof prop === 'string') { + var val = Object.getOwnPropertyDescriptor(obj, prop); + return typeof val !== 'undefined'; + } + + if (!('value' in obj) && !('writable' in obj)) { + return false; + } + + for (var key in obj) { + if (key === 'value') continue; + + if (!data.hasOwnProperty(key)) { + continue; } - }); - args.splice(lastC, 0, c); + if (typeOf(obj[key]) === data[key]) { + continue; + } + + if (typeof obj[key] !== 'undefined') { + return false; + } + } + return true; } /** - * Invokes `console.log()` when available. - * No-op when `console.log` is not a "function". - * - * @api public + * Expose `isDataDescriptor` */ -function log() { - // this hackery is required for IE8/9, where - // the `console.log` function doesn't have 'apply' - return 'object' === typeof console - && console.log - && Function.prototype.apply.call(console.log, console, arguments); -} +module.exports = isDataDescriptor; -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ -function save(namespaces) { - try { - if (null == namespaces) { - exports.storage.removeItem('debug'); - } else { - exports.storage.debug = namespaces; - } - } catch(e) {} -} +/***/ }), +/* 659 */ +/***/ (function(module, exports, __webpack_require__) { + +var isBuffer = __webpack_require__(614); +var toString = Object.prototype.toString; /** - * Load `namespaces`. + * Get the native `typeof` a value. * - * @return {String} returns the previously persisted debug modes - * @api private + * @param {*} `val` + * @return {*} Native javascript type */ -function load() { - var r; - try { - r = exports.storage.debug; - } catch(e) {} +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; + } - // If debug isn't set in LS, and we're in Electron, try to load $DEBUG - if (!r && typeof process !== 'undefined' && 'env' in process) { - r = process.env.DEBUG; + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; } - return r; -} + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; + } -/** - * Enable namespaces listed in `localStorage.debug` initially. - */ + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; + } + if (val instanceof Date) { + return 'date'; + } -exports.enable(load()); + // other objects + var type = toString.call(val); -/** - * Localstorage attempts to return the localstorage. - * - * This is necessary because safari throws - * when a user disables cookies/localstorage - * and you attempt to access it. - * - * @return {LocalStorage} - * @api private - */ + if (type === '[object RegExp]') { + return 'regexp'; + } + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; + } -function localstorage() { - try { - return window.localStorage; - } catch (e) {} -} + // buffer + if (isBuffer(val)) { + return 'buffer'; + } + + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } + + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; +}; /***/ }), -/* 626 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { - -/** - * This is the common logic for both the Node.js and web browser - * implementations of `debug()`. +"use strict"; +/*! + * static-extend * - * Expose `debug()` as the module. + * Copyright (c) 2016, Jon Schlinkert. + * Licensed under the MIT License. */ -exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; -exports.coerce = coerce; -exports.disable = disable; -exports.enable = enable; -exports.enabled = enabled; -exports.humanize = __webpack_require__(627); -/** - * The currently active debug mode names, and names to skip. - */ -exports.names = []; -exports.skips = []; +var copy = __webpack_require__(661); +var define = __webpack_require__(653); +var util = __webpack_require__(112); /** - * Map of special "%n" handling functions, for the debug "format" argument. + * Returns a function for extending the static properties, + * prototype properties, and descriptors from the `Parent` + * constructor onto `Child` constructors. * - * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + * ```js + * var extend = require('static-extend'); + * Parent.extend = extend(Parent); + * + * // optionally pass a custom merge function as the second arg + * Parent.extend = extend(Parent, function(Child) { + * Child.prototype.mixin = function(key, val) { + * Child.prototype[key] = val; + * }; + * }); + * + * // extend "child" constructors + * Parent.extend(Child); + * + * // optionally define prototype methods as the second arg + * Parent.extend(Child, { + * foo: function() {}, + * bar: function() {} + * }); + * ``` + * @param {Function} `Parent` Parent ctor + * @param {Function} `extendFn` Optional extend function for handling any necessary custom merging. Useful when updating methods that require a specific prototype. + * @param {Function} `Child` Child ctor + * @param {Object} `proto` Optionally pass additional prototype properties to inherit. + * @return {Object} + * @api public */ -exports.formatters = {}; +function extend(Parent, extendFn) { + if (typeof Parent !== 'function') { + throw new TypeError('expected Parent to be a function.'); + } -/** - * Previous log timestamp. - */ + return function(Ctor, proto) { + if (typeof Ctor !== 'function') { + throw new TypeError('expected Ctor to be a function.'); + } -var prevTime; + util.inherits(Ctor, Parent); + copy(Ctor, Parent); -/** - * Select a color. - * @param {String} namespace - * @return {Number} - * @api private - */ + // proto can be null or a plain object + if (typeof proto === 'object') { + var obj = Object.create(proto); -function selectColor(namespace) { - var hash = 0, i; + for (var k in obj) { + Ctor.prototype[k] = obj[k]; + } + } - for (i in namespace) { - hash = ((hash << 5) - hash) + namespace.charCodeAt(i); - hash |= 0; // Convert to 32bit integer - } + // keep a reference to the parent prototype + define(Ctor.prototype, '_parent_', { + configurable: true, + set: function() {}, + get: function() { + return Parent.prototype; + } + }); - return exports.colors[Math.abs(hash) % exports.colors.length]; -} + if (typeof extendFn === 'function') { + extendFn(Ctor, Parent); + } + + Ctor.extend = extend(Ctor, extendFn); + }; +}; /** - * Create a debugger with the given `namespace`. - * - * @param {String} namespace - * @return {Function} - * @api public + * Expose `extend` */ -function createDebug(namespace) { +module.exports = extend; - function debug() { - // disabled? - if (!debug.enabled) return; - var self = debug; +/***/ }), +/* 661 */ +/***/ (function(module, exports, __webpack_require__) { - // set `diff` timestamp - var curr = +new Date(); - var ms = curr - (prevTime || curr); - self.diff = ms; - self.prev = prevTime; - self.curr = curr; - prevTime = curr; +"use strict"; - // turn the `arguments` into a proper Array - var args = new Array(arguments.length); - for (var i = 0; i < args.length; i++) { - args[i] = arguments[i]; - } - args[0] = exports.coerce(args[0]); +var typeOf = __webpack_require__(662); +var copyDescriptor = __webpack_require__(663); +var define = __webpack_require__(653); - if ('string' !== typeof args[0]) { - // anything else let's inspect with %O - args.unshift('%O'); - } +/** + * Copy static properties, prototype properties, and descriptors from one object to another. + * + * ```js + * function App() {} + * var proto = App.prototype; + * App.prototype.set = function() {}; + * App.prototype.get = function() {}; + * + * var obj = {}; + * copy(obj, proto); + * ``` + * @param {Object} `receiver` + * @param {Object} `provider` + * @param {String|Array} `omit` One or more properties to omit + * @return {Object} + * @api public + */ - // apply any `formatters` transformations - var index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { - // if we encounter an escaped % then don't increase the array index - if (match === '%%') return match; - index++; - var formatter = exports.formatters[format]; - if ('function' === typeof formatter) { - var val = args[index]; - match = formatter.call(self, val); +function copy(receiver, provider, omit) { + if (!isObject(receiver)) { + throw new TypeError('expected receiving object to be an object.'); + } + if (!isObject(provider)) { + throw new TypeError('expected providing object to be an object.'); + } - // now we need to remove `args[index]` since it's inlined in the `format` - args.splice(index, 1); - index--; - } - return match; - }); + var props = nativeKeys(provider); + var keys = Object.keys(provider); + var len = props.length; + omit = arrayify(omit); - // apply env-specific formatting (colors, etc.) - exports.formatArgs.call(self, args); + while (len--) { + var key = props[len]; - var logFn = debug.log || exports.log || console.log.bind(console); - logFn.apply(self, args); + if (has(keys, key)) { + define(receiver, key, provider[key]); + } else if (!(key in receiver) && !has(omit, key)) { + copyDescriptor(receiver, provider, key); + } } +}; - debug.namespace = namespace; - debug.enabled = exports.enabled(namespace); - debug.useColors = exports.useColors(); - debug.color = selectColor(namespace); - - // env-specific initialization logic for debug instances - if ('function' === typeof exports.init) { - exports.init(debug); - } +/** + * Return true if the given value is an object or function + */ - return debug; +function isObject(val) { + return typeOf(val) === 'object' || typeof val === 'function'; } /** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. + * Returns true if an array has any of the given elements, or an + * object has any of the give keys. * - * @param {String} namespaces - * @api public + * ```js + * has(['a', 'b', 'c'], 'c'); + * //=> true + * + * has(['a', 'b', 'c'], ['c', 'z']); + * //=> true + * + * has({a: 'b', c: 'd'}, ['c', 'z']); + * //=> true + * ``` + * @param {Object} `obj` + * @param {String|Array} `val` + * @return {Boolean} */ -function enable(namespaces) { - exports.save(namespaces); +function has(obj, val) { + val = arrayify(val); + var len = val.length; - exports.names = []; - exports.skips = []; + if (isObject(obj)) { + for (var key in obj) { + if (val.indexOf(key) > -1) { + return true; + } + } - var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); - var len = split.length; + var keys = nativeKeys(obj); + return has(keys, val); + } - for (var i = 0; i < len; i++) { - if (!split[i]) continue; // ignore empty strings - namespaces = split[i].replace(/\*/g, '.*?'); - if (namespaces[0] === '-') { - exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - exports.names.push(new RegExp('^' + namespaces + '$')); + if (Array.isArray(obj)) { + var arr = obj; + while (len--) { + if (arr.indexOf(val[len]) > -1) { + return true; + } } + return false; } + + throw new TypeError('expected an array or object.'); } /** - * Disable debug output. + * Cast the given value to an array. * - * @api public + * ```js + * arrayify('foo'); + * //=> ['foo'] + * + * arrayify(['foo']); + * //=> ['foo'] + * ``` + * + * @param {String|Array} `val` + * @return {Array} */ -function disable() { - exports.enable(''); +function arrayify(val) { + return val ? (Array.isArray(val) ? val : [val]) : []; } /** - * Returns true if the given mode name is enabled, false otherwise. + * Returns true if a value has a `contructor` * - * @param {String} name + * ```js + * hasConstructor({}); + * //=> true + * + * hasConstructor(Object.create(null)); + * //=> false + * ``` + * @param {Object} `value` * @return {Boolean} - * @api public */ -function enabled(name) { - var i, len; - for (i = 0, len = exports.skips.length; i < len; i++) { - if (exports.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = exports.names.length; i < len; i++) { - if (exports.names[i].test(name)) { - return true; - } - } - return false; +function hasConstructor(val) { + return isObject(val) && typeof val.constructor !== 'undefined'; } /** - * Coerce `val`. + * Get the native `ownPropertyNames` from the constructor of the + * given `object`. An empty array is returned if the object does + * not have a constructor. * - * @param {Mixed} val - * @return {Mixed} - * @api private + * ```js + * nativeKeys({a: 'b', b: 'c', c: 'd'}) + * //=> ['a', 'b', 'c'] + * + * nativeKeys(function(){}) + * //=> ['length', 'caller'] + * ``` + * + * @param {Object} `obj` Object that has a `constructor`. + * @return {Array} Array of keys. */ -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; +function nativeKeys(val) { + if (!hasConstructor(val)) return []; + return Object.getOwnPropertyNames(val); } +/** + * Expose `copy` + */ -/***/ }), -/* 627 */ -/***/ (function(module, exports) { +module.exports = copy; /** - * Helpers. + * Expose `copy.has` for tests */ -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; -var y = d * 365.25; +module.exports.has = has; + + +/***/ }), +/* 662 */ +/***/ (function(module, exports, __webpack_require__) { + +var isBuffer = __webpack_require__(614); +var toString = Object.prototype.toString; /** - * Parse or format the given `val`. - * - * Options: - * - * - `long` verbose formatting [false] + * Get the native `typeof` a value. * - * @param {String|Number} val - * @param {Object} [options] - * @throws {Error} throw an error if val is not a non-empty string or a number - * @return {String|Number} - * @api public + * @param {*} `val` + * @return {*} Native javascript type */ -module.exports = function(val, options) { - options = options || {}; - var type = typeof val; - if (type === 'string' && val.length > 0) { - return parse(val); - } else if (type === 'number' && isNaN(val) === false) { - return options.long ? fmtLong(val) : fmtShort(val); +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; } - throw new Error( - 'val is not a non-empty string or a valid number. val=' + - JSON.stringify(val) - ); -}; -/** - * Parse the given `str` and return milliseconds. - * - * @param {String} str - * @return {Number} - * @api private - */ + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; + } -function parse(str) { - str = String(str); - if (str.length > 100) { - return; + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; } - var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( - str - ); - if (!match) { - return; + + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; } - var n = parseFloat(match[1]); - var type = (match[2] || 'ms').toLowerCase(); - switch (type) { - case 'years': - case 'year': - case 'yrs': - case 'yr': - case 'y': - return n * y; - case 'days': - case 'day': - case 'd': - return n * d; - case 'hours': - case 'hour': - case 'hrs': - case 'hr': - case 'h': - return n * h; - case 'minutes': - case 'minute': - case 'mins': - case 'min': - case 'm': - return n * m; - case 'seconds': - case 'second': - case 'secs': - case 'sec': - case 's': - return n * s; - case 'milliseconds': - case 'millisecond': - case 'msecs': - case 'msec': - case 'ms': - return n; - default: - return undefined; + if (val instanceof Date) { + return 'date'; } -} -/** - * Short format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ + // other objects + var type = toString.call(val); -function fmtShort(ms) { - if (ms >= d) { - return Math.round(ms / d) + 'd'; + if (type === '[object RegExp]') { + return 'regexp'; } - if (ms >= h) { - return Math.round(ms / h) + 'h'; + if (type === '[object Date]') { + return 'date'; } - if (ms >= m) { - return Math.round(ms / m) + 'm'; + if (type === '[object Arguments]') { + return 'arguments'; } - if (ms >= s) { - return Math.round(ms / s) + 's'; + if (type === '[object Error]') { + return 'error'; } - return ms + 'ms'; -} -/** - * Long format for `ms`. + // buffer + if (isBuffer(val)) { + return 'buffer'; + } + + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } + + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; +}; + + +/***/ }), +/* 663 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * copy-descriptor * - * @param {Number} ms - * @return {String} - * @api private + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. */ -function fmtLong(ms) { - return plural(ms, d, 'day') || - plural(ms, h, 'hour') || - plural(ms, m, 'minute') || - plural(ms, s, 'second') || - ms + ' ms'; -} + /** - * Pluralization helper. + * Copy a descriptor from one object to another. + * + * ```js + * function App() { + * this.cache = {}; + * } + * App.prototype.set = function(key, val) { + * this.cache[key] = val; + * return this; + * }; + * Object.defineProperty(App.prototype, 'count', { + * get: function() { + * return Object.keys(this.cache).length; + * } + * }); + * + * copy(App.prototype, 'count', 'len'); + * + * // create an instance + * var app = new App(); + * + * app.set('a', true); + * app.set('b', true); + * app.set('c', true); + * + * console.log(app.count); + * //=> 3 + * console.log(app.len); + * //=> 3 + * ``` + * @name copy + * @param {Object} `receiver` The target object + * @param {Object} `provider` The provider object + * @param {String} `from` The key to copy on provider. + * @param {String} `to` Optionally specify a new key name to use. + * @return {Object} + * @api public */ -function plural(ms, n, name) { - if (ms < n) { - return; +module.exports = function copyDescriptor(receiver, provider, from, to) { + if (!isObject(provider) && typeof provider !== 'function') { + to = from; + from = provider; + provider = receiver; } - if (ms < n * 1.5) { - return Math.floor(ms / n) + ' ' + name; + if (!isObject(receiver) && typeof receiver !== 'function') { + throw new TypeError('expected the first argument to be an object'); } - return Math.ceil(ms / n) + ' ' + name + 's'; + if (!isObject(provider) && typeof provider !== 'function') { + throw new TypeError('expected provider to be an object'); + } + + if (typeof to !== 'string') { + to = from; + } + if (typeof from !== 'string') { + throw new TypeError('expected key to be a string'); + } + + if (!(from in provider)) { + throw new Error('property "' + from + '" does not exist'); + } + + var val = Object.getOwnPropertyDescriptor(provider, from); + if (val) Object.defineProperty(receiver, to, val); +}; + +function isObject(val) { + return {}.toString.call(val) === '[object Object]'; } + /***/ }), -/* 628 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { -/** - * Module dependencies. - */ +"use strict"; -var tty = __webpack_require__(122); -var util = __webpack_require__(112); + +var use = __webpack_require__(665); +var define = __webpack_require__(653); +var debug = __webpack_require__(543)('snapdragon:compiler'); +var utils = __webpack_require__(667); /** - * This is the Node.js implementation of `debug()`. - * - * Expose `debug()` as the module. + * Create a new `Compiler` with the given `options`. + * @param {Object} `options` */ -exports = module.exports = __webpack_require__(626); -exports.init = init; -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; +function Compiler(options, state) { + debug('initializing', __filename); + this.options = utils.extend({source: 'string'}, options); + this.state = state || {}; + this.compilers = {}; + this.output = ''; + this.set('eos', function(node) { + return this.emit(node.val, node); + }); + this.set('noop', function(node) { + return this.emit(node.val, node); + }); + this.set('bos', function(node) { + return this.emit(node.val, node); + }); + use(this); +} /** - * Colors. + * Prototype methods */ -exports.colors = [6, 2, 3, 4, 5, 1]; +Compiler.prototype = { -/** - * Build up the default `inspectOpts` object from the environment variables. - * - * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js - */ + /** + * Throw an error message with details including the cursor position. + * @param {String} `msg` Message to use in the Error. + */ -exports.inspectOpts = Object.keys(process.env).filter(function (key) { - return /^debug_/i.test(key); -}).reduce(function (obj, key) { - // camel-case - var prop = key - .substring(6) - .toLowerCase() - .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); + error: function(msg, node) { + var pos = node.position || {start: {column: 0}}; + var message = this.options.source + ' column:' + pos.start.column + ': ' + msg; - // coerce string value into JS value - var val = process.env[key]; - if (/^(yes|on|true|enabled)$/i.test(val)) val = true; - else if (/^(no|off|false|disabled)$/i.test(val)) val = false; - else if (val === 'null') val = null; - else val = Number(val); + var err = new Error(message); + err.reason = msg; + err.column = pos.start.column; + err.source = this.pattern; - obj[prop] = val; - return obj; -}, {}); + if (this.options.silent) { + this.errors.push(err); + } else { + throw err; + } + }, -/** - * The file descriptor to write the `debug()` calls to. - * Set the `DEBUG_FD` env variable to override with another value. i.e.: - * - * $ DEBUG_FD=3 node script.js 3>debug.log - */ + /** + * Define a non-enumberable property on the `Compiler` instance. + * + * ```js + * compiler.define('foo', 'bar'); + * ``` + * @name .define + * @param {String} `key` propery name + * @param {any} `val` property value + * @return {Object} Returns the Compiler instance for chaining. + * @api public + */ + + define: function(key, val) { + define(this, key, val); + return this; + }, + + /** + * Emit `node.val` + */ + + emit: function(str, node) { + this.output += str; + return str; + }, + + /** + * Add a compiler `fn` with the given `name` + */ + + set: function(name, fn) { + this.compilers[name] = fn; + return this; + }, + + /** + * Get compiler `name`. + */ + + get: function(name) { + return this.compilers[name]; + }, + + /** + * Get the previous AST node. + */ + + prev: function(n) { + return this.ast.nodes[this.idx - (n || 1)] || { type: 'bos', val: '' }; + }, + + /** + * Get the next AST node. + */ + + next: function(n) { + return this.ast.nodes[this.idx + (n || 1)] || { type: 'eos', val: '' }; + }, -var fd = parseInt(process.env.DEBUG_FD, 10) || 2; + /** + * Visit `node`. + */ -if (1 !== fd && 2 !== fd) { - util.deprecate(function(){}, 'except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)')() -} + visit: function(node, nodes, i) { + var fn = this.compilers[node.type]; + this.idx = i; -var stream = 1 === fd ? process.stdout : - 2 === fd ? process.stderr : - createWritableStdioStream(fd); + if (typeof fn !== 'function') { + throw this.error('compiler "' + node.type + '" is not registered', node); + } + return fn.call(this, node, nodes, i); + }, -/** - * Is stdout a TTY? Colored output is enabled when `true`. - */ + /** + * Map visit over array of `nodes`. + */ -function useColors() { - return 'colors' in exports.inspectOpts - ? Boolean(exports.inspectOpts.colors) - : tty.isatty(fd); -} + mapVisit: function(nodes) { + if (!Array.isArray(nodes)) { + throw new TypeError('expected an array'); + } + var len = nodes.length; + var idx = -1; + while (++idx < len) { + this.visit(nodes[idx], nodes, idx); + } + return this; + }, -/** - * Map %o to `util.inspect()`, all on a single line. - */ + /** + * Compile `ast`. + */ -exports.formatters.o = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts) - .split('\n').map(function(str) { - return str.trim() - }).join(' '); -}; + compile: function(ast, options) { + var opts = utils.extend({}, this.options, options); + this.ast = ast; + this.parsingErrors = this.ast.errors; + this.output = ''; -/** - * Map %o to `util.inspect()`, allowing multiple lines if needed. - */ + // source map support + if (opts.sourcemap) { + var sourcemaps = __webpack_require__(686); + sourcemaps(this); + this.mapVisit(this.ast.nodes); + this.applySourceMaps(); + this.map = opts.sourcemap === 'generator' ? this.map : this.map.toJSON(); + return this; + } -exports.formatters.O = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts); + this.mapVisit(this.ast.nodes); + return this; + } }; /** - * Adds ANSI color escape codes if enabled. - * - * @api public + * Expose `Compiler` */ -function formatArgs(args) { - var name = this.namespace; - var useColors = this.useColors; +module.exports = Compiler; - if (useColors) { - var c = this.color; - var prefix = ' \u001b[3' + c + ';1m' + name + ' ' + '\u001b[0m'; - args[0] = prefix + args[0].split('\n').join('\n' + prefix); - args.push('\u001b[3' + c + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); - } else { - args[0] = new Date().toUTCString() - + ' ' + name + ' ' + args[0]; - } -} +/***/ }), +/* 665 */ +/***/ (function(module, exports, __webpack_require__) { -/** - * Invokes `util.format()` with the specified arguments and writes to `stream`. +"use strict"; +/*! + * use + * + * Copyright (c) 2015, 2017, Jon Schlinkert. + * Released under the MIT License. */ -function log() { - return stream.write(util.format.apply(util, arguments) + '\n'); -} -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ -function save(namespaces) { - if (null == namespaces) { - // If you set a process.env field to null or undefined, it gets cast to the - // string 'null' or 'undefined'. Just delete instead. - delete process.env.DEBUG; - } else { - process.env.DEBUG = namespaces; +var utils = __webpack_require__(666); + +module.exports = function base(app, opts) { + if (!utils.isObject(app) && typeof app !== 'function') { + throw new TypeError('use: expect `app` be an object or function'); } -} -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ + if (!utils.isObject(opts)) { + opts = {}; + } -function load() { - return process.env.DEBUG; -} + var prop = utils.isString(opts.prop) ? opts.prop : 'fns'; + if (!Array.isArray(app[prop])) { + utils.define(app, prop, []); + } -/** - * Copied from `node/src/node.js`. - * - * XXX: It's lame that node doesn't expose this API out-of-the-box. It also - * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. - */ + /** + * Define a plugin function to be passed to use. The only + * parameter exposed to the plugin is `app`, the object or function. + * passed to `use(app)`. `app` is also exposed as `this` in plugins. + * + * Additionally, **if a plugin returns a function, the function will + * be pushed onto the `fns` array**, allowing the plugin to be + * called at a later point by the `run` method. + * + * ```js + * var use = require('use'); + * + * // define a plugin + * function foo(app) { + * // do stuff + * } + * + * var app = function(){}; + * use(app); + * + * // register plugins + * app.use(foo); + * app.use(bar); + * app.use(baz); + * ``` + * @name .use + * @param {Function} `fn` plugin function to call + * @api public + */ -function createWritableStdioStream (fd) { - var stream; - var tty_wrap = process.binding('tty_wrap'); + utils.define(app, 'use', use); - // Note stream._type is used for test-module-load-list.js + /** + * Run all plugins on `fns`. Any plugin that returns a function + * when called by `use` is pushed onto the `fns` array. + * + * ```js + * var config = {}; + * app.run(config); + * ``` + * @name .run + * @param {Object} `value` Object to be modified by plugins. + * @return {Object} Returns the object passed to `run` + * @api public + */ - switch (tty_wrap.guessHandleType(fd)) { - case 'TTY': - stream = new tty.WriteStream(fd); - stream._type = 'tty'; + utils.define(app, 'run', function(val) { + if (!utils.isObject(val)) return; + decorate(val); - // Hack to have stream not keep the event loop alive. - // See https://github.com/joyent/node/issues/1726 - if (stream._handle && stream._handle.unref) { - stream._handle.unref(); - } - break; + var self = this || app; + var fns = self[prop]; + var len = fns.length; + var idx = -1; - case 'FILE': - var fs = __webpack_require__(134); - stream = new fs.SyncWriteStream(fd, { autoClose: false }); - stream._type = 'fs'; - break; + while (++idx < len) { + val.use(fns[idx]); + } + return val; + }); - case 'PIPE': - case 'TCP': - var net = __webpack_require__(629); - stream = new net.Socket({ - fd: fd, - readable: false, - writable: true - }); + /** + * Call plugin `fn`. If a function is returned push it into the + * `fns` array to be called by the `run` method. + */ - // FIXME Should probably have an option in net.Socket to create a - // stream from an existing fd which is writable only. But for now - // we'll just add this hack and set the `readable` member to false. - // Test: ./node test/fixtures/echo.js < /etc/passwd - stream.readable = false; - stream.read = null; - stream._type = 'pipe'; + function use(fn, options) { + if (typeof fn !== 'function') { + throw new TypeError('.use expects `fn` be a function'); + } - // FIXME Hack to have stream not keep the event loop alive. - // See https://github.com/joyent/node/issues/1726 - if (stream._handle && stream._handle.unref) { - stream._handle.unref(); - } - break; + var self = this || app; + if (typeof opts.fn === 'function') { + opts.fn.call(self, self, options); + } - default: - // Probably an error on in uv_guess_handle() - throw new Error('Implement me. Unknown stream file type!'); + var plugin = fn.call(self, self); + if (typeof plugin === 'function') { + var fns = self[prop]; + fns.push(plugin); + } + return self; } - // For supporting legacy API we put the FD here. - stream.fd = fd; + /** + * Ensure the `.use` method exists on `val` + */ - stream._isStdio = true; + function decorate(val) { + if (!val.use || !val.run) { + base(val); + } + } - return stream; -} + return app; +}; -/** - * Init logic for `debug` instances. - * - * Create a new `inspectOpts` object in case `useColors` is set - * differently for a particular `debug` instance. - */ -function init (debug) { - debug.inspectOpts = {}; +/***/ }), +/* 666 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var utils = {}; + - var keys = Object.keys(exports.inspectOpts); - for (var i = 0; i < keys.length; i++) { - debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; - } -} /** - * Enable namespaces listed in `process.env.DEBUG` initially. + * Lazily required module dependencies */ -exports.enable(load()); +utils.define = __webpack_require__(653); +utils.isObject = __webpack_require__(590); -/***/ }), -/* 629 */ -/***/ (function(module, exports) { +utils.isString = function(val) { + return val && typeof val === 'string'; +}; + +/** + * Expose `utils` modules + */ + +module.exports = utils; -module.exports = require("net"); /***/ }), -/* 630 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72645,9 +75529,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(560); -exports.SourceMap = __webpack_require__(631); -exports.sourceMapResolve = __webpack_require__(642); +exports.extend = __webpack_require__(603); +exports.SourceMap = __webpack_require__(668); +exports.sourceMapResolve = __webpack_require__(679); /** * Convert backslash in the given string to forward slashes @@ -72690,7 +75574,7 @@ exports.last = function(arr, n) { /***/ }), -/* 631 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72698,13 +75582,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(632).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(638).SourceMapConsumer; -exports.SourceNode = __webpack_require__(641).SourceNode; +exports.SourceMapGenerator = __webpack_require__(669).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(675).SourceMapConsumer; +exports.SourceNode = __webpack_require__(678).SourceNode; /***/ }), -/* 632 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72714,10 +75598,10 @@ exports.SourceNode = __webpack_require__(641).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(633); -var util = __webpack_require__(635); -var ArraySet = __webpack_require__(636).ArraySet; -var MappingList = __webpack_require__(637).MappingList; +var base64VLQ = __webpack_require__(670); +var util = __webpack_require__(672); +var ArraySet = __webpack_require__(673).ArraySet; +var MappingList = __webpack_require__(674).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -73126,7 +76010,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 633 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73166,7 +76050,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(634); +var base64 = __webpack_require__(671); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -73272,7 +76156,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 634 */ +/* 671 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73345,7 +76229,7 @@ exports.decode = function (charCode) { /***/ }), -/* 635 */ +/* 672 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73768,7 +76652,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 636 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73778,7 +76662,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(635); +var util = __webpack_require__(672); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73895,7 +76779,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 637 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73905,7 +76789,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(635); +var util = __webpack_require__(672); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73980,7 +76864,7 @@ exports.MappingList = MappingList; /***/ }), -/* 638 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73990,11 +76874,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(635); -var binarySearch = __webpack_require__(639); -var ArraySet = __webpack_require__(636).ArraySet; -var base64VLQ = __webpack_require__(633); -var quickSort = __webpack_require__(640).quickSort; +var util = __webpack_require__(672); +var binarySearch = __webpack_require__(676); +var ArraySet = __webpack_require__(673).ArraySet; +var base64VLQ = __webpack_require__(670); +var quickSort = __webpack_require__(677).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -75068,7 +77952,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 639 */ +/* 676 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75185,7 +78069,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 640 */ +/* 677 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75305,7 +78189,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 641 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75315,8 +78199,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(632).SourceMapGenerator; -var util = __webpack_require__(635); +var SourceMapGenerator = __webpack_require__(669).SourceMapGenerator; +var util = __webpack_require__(672); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75724,17 +78608,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 642 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(643) -var resolveUrl = __webpack_require__(644) -var decodeUriComponent = __webpack_require__(645) -var urix = __webpack_require__(647) -var atob = __webpack_require__(648) +var sourceMappingURL = __webpack_require__(680) +var resolveUrl = __webpack_require__(681) +var decodeUriComponent = __webpack_require__(682) +var urix = __webpack_require__(684) +var atob = __webpack_require__(685) @@ -76032,7 +78916,7 @@ module.exports = { /***/ }), -/* 643 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -76095,7 +78979,7 @@ void (function(root, factory) { /***/ }), -/* 644 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -76113,13 +78997,13 @@ module.exports = resolveUrl /***/ }), -/* 645 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(646) +var decodeUriComponent = __webpack_require__(683) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -76130,7 +79014,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 646 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76231,7 +79115,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 647 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -76254,7 +79138,7 @@ module.exports = urix /***/ }), -/* 648 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76268,7 +79152,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 649 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76276,8 +79160,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(610); -var utils = __webpack_require__(630); +var define = __webpack_require__(653); +var utils = __webpack_require__(667); /** * Expose `mixin()`. @@ -76420,19 +79304,19 @@ exports.comment = function(node) { /***/ }), -/* 650 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(622); +var use = __webpack_require__(665); var util = __webpack_require__(112); -var Cache = __webpack_require__(651); -var define = __webpack_require__(610); -var debug = __webpack_require__(624)('snapdragon:parser'); -var Position = __webpack_require__(652); -var utils = __webpack_require__(630); +var Cache = __webpack_require__(688); +var define = __webpack_require__(653); +var debug = __webpack_require__(543)('snapdragon:parser'); +var Position = __webpack_require__(689); +var utils = __webpack_require__(667); /** * Create a new `Parser` with the given `input` and `options`. @@ -76960,7 +79844,7 @@ module.exports = Parser; /***/ }), -/* 651 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77067,13 +79951,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 652 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(610); +var define = __webpack_require__(653); /** * Store position for a node @@ -77088,14 +79972,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 653 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(654); -var assignSymbols = __webpack_require__(555); +var isExtendable = __webpack_require__(691); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77155,7 +80039,7 @@ function isEnum(obj, key) { /***/ }), -/* 654 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77168,7 +80052,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(554); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -77176,14 +80060,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 655 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(656); -var extglob = __webpack_require__(670); +var nanomatch = __webpack_require__(693); +var extglob = __webpack_require__(707); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -77260,7 +80144,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 656 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77271,17 +80155,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(539); -var extend = __webpack_require__(657); +var toRegex = __webpack_require__(582); +var extend = __webpack_require__(694); /** * Local dependencies */ -var compilers = __webpack_require__(659); -var parsers = __webpack_require__(660); -var cache = __webpack_require__(663); -var utils = __webpack_require__(665); +var compilers = __webpack_require__(696); +var parsers = __webpack_require__(697); +var cache = __webpack_require__(700); +var utils = __webpack_require__(702); var MAX_LENGTH = 1024 * 64; /** @@ -78105,14 +80989,14 @@ module.exports = nanomatch; /***/ }), -/* 657 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(658); -var assignSymbols = __webpack_require__(555); +var isExtendable = __webpack_require__(695); +var assignSymbols = __webpack_require__(598); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -78172,7 +81056,7 @@ function isEnum(obj, key) { /***/ }), -/* 658 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78185,7 +81069,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(554); +var isPlainObject = __webpack_require__(597); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -78193,7 +81077,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 659 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78539,15 +81423,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 660 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(556); -var toRegex = __webpack_require__(539); -var isOdd = __webpack_require__(661); +var regexNot = __webpack_require__(599); +var toRegex = __webpack_require__(582); +var isOdd = __webpack_require__(698); /** * Characters to use in negation regex (we want to "not" match @@ -78933,7 +81817,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 661 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78946,7 +81830,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(662); +var isNumber = __webpack_require__(699); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78960,7 +81844,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 662 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78988,14 +81872,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 663 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(664))(); +module.exports = new (__webpack_require__(701))(); /***/ }), -/* 664 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79008,7 +81892,7 @@ module.exports = new (__webpack_require__(664))(); -var MapCache = __webpack_require__(651); +var MapCache = __webpack_require__(688); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -79130,7 +82014,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 665 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79143,14 +82027,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(666)(); -var Snapdragon = __webpack_require__(581); -utils.define = __webpack_require__(667); -utils.diff = __webpack_require__(668); -utils.extend = __webpack_require__(657); -utils.pick = __webpack_require__(669); -utils.typeOf = __webpack_require__(549); -utils.unique = __webpack_require__(559); +var isWindows = __webpack_require__(703)(); +var Snapdragon = __webpack_require__(624); +utils.define = __webpack_require__(704); +utils.diff = __webpack_require__(705); +utils.extend = __webpack_require__(694); +utils.pick = __webpack_require__(706); +utils.typeOf = __webpack_require__(592); +utils.unique = __webpack_require__(602); /** * Returns true if the given value is effectively an empty string @@ -79516,7 +82400,7 @@ utils.unixify = function(options) { /***/ }), -/* 666 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -79544,7 +82428,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 667 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79557,8 +82441,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(547); -var isDescriptor = __webpack_require__(548); +var isobject = __webpack_require__(590); +var isDescriptor = __webpack_require__(591); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -79589,7 +82473,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 668 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79643,7 +82527,7 @@ function diffArray(one, two) { /***/ }), -/* 669 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79656,7 +82540,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(547); +var isObject = __webpack_require__(590); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -79685,7 +82569,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 670 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79695,18 +82579,18 @@ module.exports = function pick(obj, keys) { * Module dependencies */ -var extend = __webpack_require__(560); -var unique = __webpack_require__(559); -var toRegex = __webpack_require__(539); +var extend = __webpack_require__(603); +var unique = __webpack_require__(602); +var toRegex = __webpack_require__(582); /** * Local dependencies */ -var compilers = __webpack_require__(671); -var parsers = __webpack_require__(677); -var Extglob = __webpack_require__(680); -var utils = __webpack_require__(679); +var compilers = __webpack_require__(708); +var parsers = __webpack_require__(714); +var Extglob = __webpack_require__(717); +var utils = __webpack_require__(716); var MAX_LENGTH = 1024 * 64; /** @@ -80023,13 +82907,13 @@ module.exports = extglob; /***/ }), -/* 671 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(672); +var brackets = __webpack_require__(709); /** * Extglob compilers @@ -80199,7 +83083,7 @@ module.exports = function(extglob) { /***/ }), -/* 672 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80209,17 +83093,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(673); -var parsers = __webpack_require__(675); +var compilers = __webpack_require__(710); +var parsers = __webpack_require__(712); /** * Module dependencies */ -var debug = __webpack_require__(624)('expand-brackets'); -var extend = __webpack_require__(560); -var Snapdragon = __webpack_require__(581); -var toRegex = __webpack_require__(539); +var debug = __webpack_require__(543)('expand-brackets'); +var extend = __webpack_require__(603); +var Snapdragon = __webpack_require__(624); +var toRegex = __webpack_require__(582); /** * Parses the given POSIX character class `pattern` and returns a @@ -80417,13 +83301,13 @@ module.exports = brackets; /***/ }), -/* 673 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(674); +var posix = __webpack_require__(711); module.exports = function(brackets) { brackets.compiler @@ -80511,7 +83395,7 @@ module.exports = function(brackets) { /***/ }), -/* 674 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80540,14 +83424,14 @@ module.exports = { /***/ }), -/* 675 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(676); -var define = __webpack_require__(610); +var utils = __webpack_require__(713); +var define = __webpack_require__(653); /** * Text regex @@ -80766,14 +83650,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 676 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(539); -var regexNot = __webpack_require__(556); +var toRegex = __webpack_require__(582); +var regexNot = __webpack_require__(599); var cached; /** @@ -80807,15 +83691,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 677 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(672); -var define = __webpack_require__(678); -var utils = __webpack_require__(679); +var brackets = __webpack_require__(709); +var define = __webpack_require__(715); +var utils = __webpack_require__(716); /** * Characters to use in text regex (we want to "not" match @@ -80970,7 +83854,7 @@ module.exports = parsers; /***/ }), -/* 678 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80983,7 +83867,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(548); +var isDescriptor = __webpack_require__(591); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -81008,14 +83892,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 679 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(556); -var Cache = __webpack_require__(664); +var regex = __webpack_require__(599); +var Cache = __webpack_require__(701); /** * Utils @@ -81084,7 +83968,7 @@ utils.createRegex = function(str) { /***/ }), -/* 680 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81094,16 +83978,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(581); -var define = __webpack_require__(678); -var extend = __webpack_require__(560); +var Snapdragon = __webpack_require__(624); +var define = __webpack_require__(715); +var extend = __webpack_require__(603); /** * Local dependencies */ -var compilers = __webpack_require__(671); -var parsers = __webpack_require__(677); +var compilers = __webpack_require__(708); +var parsers = __webpack_require__(714); /** * Customize Snapdragon parser and renderer @@ -81169,16 +84053,16 @@ module.exports = Extglob; /***/ }), -/* 681 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(670); -var nanomatch = __webpack_require__(656); -var regexNot = __webpack_require__(556); -var toRegex = __webpack_require__(539); +var extglob = __webpack_require__(707); +var nanomatch = __webpack_require__(693); +var regexNot = __webpack_require__(599); +var toRegex = __webpack_require__(582); var not; /** @@ -81259,14 +84143,14 @@ function textRegex(pattern) { /***/ }), -/* 682 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(664))(); +module.exports = new (__webpack_require__(701))(); /***/ }), -/* 683 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81279,13 +84163,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(581); -utils.define = __webpack_require__(684); -utils.diff = __webpack_require__(668); -utils.extend = __webpack_require__(653); -utils.pick = __webpack_require__(669); -utils.typeOf = __webpack_require__(549); -utils.unique = __webpack_require__(559); +var Snapdragon = __webpack_require__(624); +utils.define = __webpack_require__(721); +utils.diff = __webpack_require__(705); +utils.extend = __webpack_require__(690); +utils.pick = __webpack_require__(706); +utils.typeOf = __webpack_require__(592); +utils.unique = __webpack_require__(602); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -81582,7 +84466,7 @@ utils.unixify = function(options) { /***/ }), -/* 684 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81595,8 +84479,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(547); -var isDescriptor = __webpack_require__(548); +var isobject = __webpack_require__(590); +var isDescriptor = __webpack_require__(591); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -81627,7 +84511,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 685 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81646,9 +84530,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(686); -var reader_1 = __webpack_require__(699); -var fs_stream_1 = __webpack_require__(703); +var readdir = __webpack_require__(723); +var reader_1 = __webpack_require__(736); +var fs_stream_1 = __webpack_require__(740); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81709,15 +84593,15 @@ exports.default = ReaderAsync; /***/ }), -/* 686 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(687); -const readdirAsync = __webpack_require__(695); -const readdirStream = __webpack_require__(698); +const readdirSync = __webpack_require__(724); +const readdirAsync = __webpack_require__(732); +const readdirStream = __webpack_require__(735); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81801,7 +84685,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 687 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81809,11 +84693,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(688); +const DirectoryReader = __webpack_require__(725); let syncFacade = { - fs: __webpack_require__(693), - forEach: __webpack_require__(694), + fs: __webpack_require__(730), + forEach: __webpack_require__(731), sync: true }; @@ -81842,7 +84726,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 688 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81851,9 +84735,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(689); -const stat = __webpack_require__(691); -const call = __webpack_require__(692); +const normalizeOptions = __webpack_require__(726); +const stat = __webpack_require__(728); +const call = __webpack_require__(729); /** * Asynchronously reads the contents of a directory and streams the results @@ -82229,14 +85113,14 @@ module.exports = DirectoryReader; /***/ }), -/* 689 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(690); +const globToRegExp = __webpack_require__(727); module.exports = normalizeOptions; @@ -82413,7 +85297,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 690 */ +/* 727 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82550,13 +85434,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 691 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(692); +const call = __webpack_require__(729); module.exports = stat; @@ -82631,7 +85515,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 692 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82692,14 +85576,14 @@ function callOnce (fn) { /***/ }), -/* 693 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(692); +const call = __webpack_require__(729); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82763,7 +85647,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 694 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82792,7 +85676,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 695 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82800,12 +85684,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(696); -const DirectoryReader = __webpack_require__(688); +const maybe = __webpack_require__(733); +const DirectoryReader = __webpack_require__(725); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(697), + forEach: __webpack_require__(734), async: true }; @@ -82847,7 +85731,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 696 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82874,7 +85758,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 697 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82910,7 +85794,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 698 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82918,11 +85802,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(688); +const DirectoryReader = __webpack_require__(725); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(697), + forEach: __webpack_require__(734), async: true }; @@ -82942,16 +85826,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 699 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(700); -var entry_1 = __webpack_require__(702); -var pathUtil = __webpack_require__(701); +var deep_1 = __webpack_require__(737); +var entry_1 = __webpack_require__(739); +var pathUtil = __webpack_require__(738); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -83017,14 +85901,14 @@ exports.default = Reader; /***/ }), -/* 700 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(701); -var patternUtils = __webpack_require__(533); +var pathUtils = __webpack_require__(738); +var patternUtils = __webpack_require__(576); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -83107,7 +85991,7 @@ exports.default = DeepFilter; /***/ }), -/* 701 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83138,14 +86022,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 702 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(701); -var patternUtils = __webpack_require__(533); +var pathUtils = __webpack_require__(738); +var patternUtils = __webpack_require__(576); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -83230,7 +86114,7 @@ exports.default = EntryFilter; /***/ }), -/* 703 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83250,8 +86134,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(704); -var fs_1 = __webpack_require__(708); +var fsStat = __webpack_require__(741); +var fs_1 = __webpack_require__(745); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -83301,14 +86185,14 @@ exports.default = FileSystemStream; /***/ }), -/* 704 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(705); -const statProvider = __webpack_require__(707); +const optionsManager = __webpack_require__(742); +const statProvider = __webpack_require__(744); /** * Asynchronous API. */ @@ -83339,13 +86223,13 @@ exports.statSync = statSync; /***/ }), -/* 705 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(706); +const fsAdapter = __webpack_require__(743); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83358,7 +86242,7 @@ exports.prepare = prepare; /***/ }), -/* 706 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83381,7 +86265,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 707 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83433,7 +86317,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 708 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83464,7 +86348,7 @@ exports.default = FileSystem; /***/ }), -/* 709 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83484,9 +86368,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(686); -var reader_1 = __webpack_require__(699); -var fs_stream_1 = __webpack_require__(703); +var readdir = __webpack_require__(723); +var reader_1 = __webpack_require__(736); +var fs_stream_1 = __webpack_require__(740); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83554,7 +86438,7 @@ exports.default = ReaderStream; /***/ }), -/* 710 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83573,9 +86457,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(686); -var reader_1 = __webpack_require__(699); -var fs_sync_1 = __webpack_require__(711); +var readdir = __webpack_require__(723); +var reader_1 = __webpack_require__(736); +var fs_sync_1 = __webpack_require__(748); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83635,7 +86519,7 @@ exports.default = ReaderSync; /***/ }), -/* 711 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83654,8 +86538,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(704); -var fs_1 = __webpack_require__(708); +var fsStat = __webpack_require__(741); +var fs_1 = __webpack_require__(745); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83701,7 +86585,7 @@ exports.default = FileSystemSync; /***/ }), -/* 712 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83717,7 +86601,7 @@ exports.flatten = flatten; /***/ }), -/* 713 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83738,13 +86622,13 @@ exports.merge = merge; /***/ }), -/* 714 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(715); +const pathType = __webpack_require__(752); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83810,13 +86694,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 715 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(716); +const pify = __webpack_require__(753); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83859,7 +86743,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 716 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83950,17 +86834,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 717 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(529); -const gitIgnore = __webpack_require__(718); -const pify = __webpack_require__(719); -const slash = __webpack_require__(720); +const fastGlob = __webpack_require__(572); +const gitIgnore = __webpack_require__(755); +const pify = __webpack_require__(756); +const slash = __webpack_require__(757); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -84058,7 +86942,7 @@ module.exports.sync = options => { /***/ }), -/* 718 */ +/* 755 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84527,7 +87411,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 719 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84602,7 +87486,7 @@ module.exports = (input, options) => { /***/ }), -/* 720 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84620,7 +87504,7 @@ module.exports = input => { /***/ }), -/* 721 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84633,7 +87517,7 @@ module.exports = input => { -var isGlob = __webpack_require__(722); +var isGlob = __webpack_require__(759); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84653,7 +87537,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 722 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84684,17 +87568,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 723 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(724); -const CpFileError = __webpack_require__(727); -const fs = __webpack_require__(729); -const ProgressEmitter = __webpack_require__(732); +const pEvent = __webpack_require__(761); +const CpFileError = __webpack_require__(764); +const fs = __webpack_require__(766); +const ProgressEmitter = __webpack_require__(769); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84808,12 +87692,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 724 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(725); +const pTimeout = __webpack_require__(762); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -85104,12 +87988,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 725 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(726); +const pFinally = __webpack_require__(763); class TimeoutError extends Error { constructor(message) { @@ -85155,7 +88039,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 726 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85177,12 +88061,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 727 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(728); +const NestedError = __webpack_require__(765); class CpFileError extends NestedError { constructor(message, nested) { @@ -85196,7 +88080,7 @@ module.exports = CpFileError; /***/ }), -/* 728 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -85252,16 +88136,16 @@ module.exports = NestedError; /***/ }), -/* 729 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(730); -const pEvent = __webpack_require__(724); -const CpFileError = __webpack_require__(727); +const makeDir = __webpack_require__(767); +const pEvent = __webpack_require__(761); +const CpFileError = __webpack_require__(764); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85358,7 +88242,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 730 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85366,7 +88250,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(731); +const semver = __webpack_require__(768); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85521,7 +88405,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 731 */ +/* 768 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -87123,7 +90007,7 @@ function coerce (version, options) { /***/ }), -/* 732 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87164,7 +90048,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 733 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87210,12 +90094,12 @@ exports.default = module.exports; /***/ }), -/* 734 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(735); +const pMap = __webpack_require__(772); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -87232,7 +90116,7 @@ module.exports.default = pFilter; /***/ }), -/* 735 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87311,12 +90195,12 @@ module.exports.default = pMap; /***/ }), -/* 736 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(728); +const NestedError = __webpack_require__(765); class CpyError extends NestedError { constructor(message, nested) { @@ -87330,16 +90214,16 @@ module.exports = CpyError; /***/ }), -/* 737 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const arrayUnion = __webpack_require__(738); +const arrayUnion = __webpack_require__(775); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(529); -const dirGlob = __webpack_require__(739); -const gitignore = __webpack_require__(743); +const fastGlob = __webpack_require__(572); +const dirGlob = __webpack_require__(776); +const gitignore = __webpack_require__(780); const DEFAULT_FILTER = () => false; @@ -87465,12 +90349,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 738 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(528); +var arrayUniq = __webpack_require__(571); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -87478,14 +90362,14 @@ module.exports = function () { /***/ }), -/* 739 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const arrify = __webpack_require__(740); -const pathType = __webpack_require__(741); +const arrify = __webpack_require__(777); +const pathType = __webpack_require__(778); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; const getPath = filepath => filepath[0] === '!' ? filepath.slice(1) : filepath; @@ -87533,7 +90417,7 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 740 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87548,13 +90432,13 @@ module.exports = function (val) { /***/ }), -/* 741 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(742); +const pify = __webpack_require__(779); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -87597,7 +90481,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 742 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87688,17 +90572,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 743 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(529); -const gitIgnore = __webpack_require__(744); -const pify = __webpack_require__(742); -const slash = __webpack_require__(745); +const fastGlob = __webpack_require__(572); +const gitIgnore = __webpack_require__(781); +const pify = __webpack_require__(779); +const slash = __webpack_require__(782); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -87790,7 +90674,7 @@ module.exports.sync = o => { /***/ }), -/* 744 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88222,7 +91106,7 @@ typeof process !== 'undefined' && (process.env && process.env.IGNORE_TEST_WIN32 /***/ }), -/* 745 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88240,7 +91124,7 @@ module.exports = function (str) { /***/ }), -/* 746 */ +/* 783 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -88248,13 +91132,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return buildNonBazelProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getProductionProjects", function() { return getProductionProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProject", function() { return buildProject; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(522); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(519); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(562); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 544bfd5587e00..4a6a43ff2d91f 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -23,6 +23,11 @@ export const BootstrapCommand: ICommand = { description: 'Install dependencies and crosslink projects', name: 'bootstrap', + reportTiming: { + group: 'bootstrap', + id: 'overall time', + }, + async run(projects, projectGraph, { options, kbn, rootPath }) { const nonBazelProjectsOnly = await getNonBazelProjectsOnly(projects); const batchedNonBazelProjects = topologicallyBatchProjects(nonBazelProjectsOnly, projectGraph); diff --git a/packages/kbn-pm/src/commands/index.ts b/packages/kbn-pm/src/commands/index.ts index 9f02a6bf7038a..0ab6bc9c7808a 100644 --- a/packages/kbn-pm/src/commands/index.ts +++ b/packages/kbn-pm/src/commands/index.ts @@ -18,6 +18,10 @@ export interface ICommandConfig { export interface ICommand { name: string; description: string; + reportTiming?: { + group: string; + id: string; + }; run: (projects: ProjectMap, projectGraph: ProjectGraph, config: ICommandConfig) => Promise; } diff --git a/packages/kbn-pm/src/run.ts b/packages/kbn-pm/src/run.ts index 5ffe2beeeb77b..e5d74cee65ba7 100644 --- a/packages/kbn-pm/src/run.ts +++ b/packages/kbn-pm/src/run.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { CiStatsReporter } from '@kbn/dev-utils/ci_stats_reporter'; + import { ICommand, ICommandConfig } from './commands'; import { CliError } from './utils/errors'; import { log } from './utils/log'; @@ -14,10 +16,13 @@ import { renderProjectsTree } from './utils/projects_tree'; import { Kibana } from './utils/kibana'; export async function runCommand(command: ICommand, config: Omit) { + const runStartTime = Date.now(); + let kbn; + try { log.debug(`Running [${command.name}] command from [${config.rootPath}]`); - const kbn = await Kibana.loadFrom(config.rootPath); + kbn = await Kibana.loadFrom(config.rootPath); const projects = kbn.getFilteredProjects({ skipKibanaPlugins: Boolean(config.options['skip-kibana-plugins']), ossOnly: Boolean(config.options.oss), @@ -41,7 +46,46 @@ export async function runCommand(command: ICommand, config: Omit { let logger: ReturnType; let configService: ReturnType; let coreContext: CoreContext; + let service: EnvironmentService; - beforeEach(() => { - jest.clearAllMocks(); + beforeEach(async () => { logger = loggingSystemMock.create(); configService = getConfigService(); coreContext = mockCoreContext.create({ logger, configService }); + + service = new EnvironmentService(coreContext); + }); + + afterEach(() => { + jest.clearAllMocks(); }); describe('#setup()', () => { it('calls resolveInstanceUuid with correct parameters', async () => { - const service = new EnvironmentService(coreContext); await service.setup(); + expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); expect(resolveInstanceUuid).toHaveBeenCalledWith({ pathConfig, @@ -83,8 +89,8 @@ describe('UuidService', () => { }); it('calls createDataFolder with correct parameters', async () => { - const service = new EnvironmentService(coreContext); await service.setup(); + expect(createDataFolder).toHaveBeenCalledTimes(1); expect(createDataFolder).toHaveBeenCalledWith({ pathConfig, @@ -93,8 +99,8 @@ describe('UuidService', () => { }); it('calls writePidFile with correct parameters', async () => { - const service = new EnvironmentService(coreContext); await service.setup(); + expect(writePidFile).toHaveBeenCalledTimes(1); expect(writePidFile).toHaveBeenCalledWith({ pidConfig, @@ -103,9 +109,31 @@ describe('UuidService', () => { }); it('returns the uuid resolved from resolveInstanceUuid', async () => { - const service = new EnvironmentService(coreContext); const setup = await service.setup(); + expect(setup.instanceUuid).toEqual('SOME_UUID'); }); + + describe('process warnings', () => { + it('logs warnings coming from the process', async () => { + await service.setup(); + + const warning = new Error('something went wrong'); + process.emit('warning', warning); + + expect(logger.get('process').warn).toHaveBeenCalledTimes(1); + expect(logger.get('process').warn).toHaveBeenCalledWith(warning); + }); + + it('does not log deprecation warnings', async () => { + await service.setup(); + + const warning = new Error('something went wrong'); + warning.name = 'DeprecationWarning'; + process.emit('warning', warning); + + expect(logger.get('process').warn).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index a6bcdf4c35661..e652622049cfa 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -30,11 +30,13 @@ export interface InternalEnvironmentServiceSetup { /** @internal */ export class EnvironmentService { private readonly log: Logger; + private readonly processLogger: Logger; private readonly configService: IConfigService; private uuid: string = ''; constructor(core: CoreContext) { this.log = core.logger.get('environment'); + this.processLogger = core.logger.get('process'); this.configService = core.configService; } @@ -50,6 +52,14 @@ export class EnvironmentService { this.log.warn(`Detected an unhandled Promise rejection.\n${reason}`); }); + process.on('warning', (warning) => { + // deprecation warnings do no reflect a current problem for the user and should be filtered out. + if (warning.name === 'DeprecationWarning') { + return; + } + this.processLogger.warn(warning); + }); + await createDataFolder({ pathConfig, logger: this.log }); await writePidFile({ pidConfig, logger: this.log }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 1cedddc1d1e5c..b0510bc414bf8 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -225,16 +225,15 @@ export class HttpServer { private getAuthOption( authRequired: RouteConfigOptions['authRequired'] = true - ): undefined | false | { mode: 'required' | 'optional' | 'try' } { + ): undefined | false | { mode: 'required' | 'try' } { if (this.authRegistered === false) return undefined; if (authRequired === true) { return { mode: 'required' }; } if (authRequired === 'optional') { - return { mode: 'optional' }; - } - if (authRequired === 'try') { + // we want to use HAPI `try` mode and not `optional` to not throw unauthorized errors when the user + // has invalid or expired credentials return { mode: 'try' }; } if (authRequired === false) { diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 33bdf28c6d901..6c11534df0d11 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -136,37 +136,6 @@ describe('http service', () => { await root.start(); await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); }); - - it('returns true if authenticated on a route with "try" auth', async () => { - const { http } = await root.setup(); - const { createRouter, auth, registerAuth } = http; - - registerAuth((req, res, toolkit) => toolkit.authenticated()); - const router = createRouter(''); - router.get( - { path: '/is-auth', validate: false, options: { authRequired: 'try' } }, - (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) - ); - - await root.start(); - await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true }); - }); - - it('returns false if not authenticated on a route with "try" auth', async () => { - const { http } = await root.setup(); - const { createRouter, auth, registerAuth } = http; - - registerAuth((req, res, toolkit) => toolkit.notHandled()); - - const router = createRouter(''); - router.get( - { path: '/is-auth', validate: false, options: { authRequired: 'try' } }, - (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) - ); - - await root.start(); - await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); - }); }); describe('#get()', () => { it('returns authenticated status and allow associate auth state with request', async () => { diff --git a/src/core/server/http/integration_tests/http_auth.test.ts b/src/core/server/http/integration_tests/http_auth.test.ts index 2aa4d2796a6f2..0696deb9c07ae 100644 --- a/src/core/server/http/integration_tests/http_auth.test.ts +++ b/src/core/server/http/integration_tests/http_auth.test.ts @@ -146,46 +146,6 @@ describe('http auth', () => { await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); }); - it('blocks access when auth returns `unauthorized`', async () => { - const { http } = await root.setup(); - const { registerAuth, createRouter, auth } = http; - - registerAuth((req, res, toolkit) => res.unauthorized()); - - const router = createRouter(''); - registerRoute(router, auth, 'optional'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(401); - }); - }); - describe('when authRequired is `try`', () => { - it('allows authenticated access when auth returns `authenticated`', async () => { - const { http } = await root.setup(); - const { registerAuth, createRouter, auth } = http; - - registerAuth((req, res, toolkit) => toolkit.authenticated()); - - const router = createRouter(''); - registerRoute(router, auth, 'try'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: true }); - }); - - it('allows anonymous access when auth returns `notHandled`', async () => { - const { http } = await root.setup(); - const { registerAuth, createRouter, auth } = http; - - registerAuth((req, res, toolkit) => toolkit.notHandled()); - - const router = createRouter(''); - registerRoute(router, auth, 'try'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); - }); - it('allows anonymous access when auth returns `unauthorized`', async () => { const { http } = await root.setup(); const { registerAuth, createRouter, auth } = http; @@ -193,7 +153,7 @@ describe('http auth', () => { registerAuth((req, res, toolkit) => res.unauthorized()); const router = createRouter(''); - registerRoute(router, auth, 'try'); + registerRoute(router, auth, 'optional'); await root.start(); await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); @@ -234,16 +194,5 @@ describe('http auth', () => { await root.start(); await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); }); - - it('allow access to resources when `authRequired` is `try`', async () => { - const { http } = await root.setup(); - const { createRouter, auth } = http; - - const router = createRouter(''); - registerRoute(router, auth, 'try'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); - }); }); }); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 248b1e1278c4c..03324dc6c722f 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -114,19 +114,30 @@ describe('Options', () => { }); }); - it('User with invalid credentials cannot access a route', async () => { - const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + it('User with invalid credentials can access a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); const router = createRouter('/'); registerAuth((req, res, toolkit) => res.unauthorized()); router.get( { path: '/', validate: false, options: { authRequired: 'optional' } }, - (context, req, res) => res.ok({ body: 'ok' }) + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) ); await server.start(); - await supertest(innerServer.listener).get('/').expect(401); + await supertest(innerServer.listener).get('/').expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); }); it('does not redirect user and allows access to a resource', async () => { diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 758bfad874d90..167cf0747b4c1 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -123,7 +123,7 @@ export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; /** * User has no credentials. - * Allows user to access a resource when authRequired is 'optional' or 'try' + * Allows user to access a resource when authRequired is 'optional' * Rejects a request when authRequired: true * */ notHandled: () => AuthResult; diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 879b48f7253a0..77b40ca5995bb 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -108,14 +108,12 @@ export interface RouteConfigOptions { * Defines authentication mode for a route: * - true. A user has to have valid credentials to access a resource * - false. A user can access a resource without any credentials. - * - 'optional'. A user can access a resource if has valid credentials or no credentials at all. + * - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. * Can be useful when we grant access to a resource but want to identify a user if possible. - * - 'try'. A user can access a resource with valid, invalid or without any credentials. - * Users with valid credentials will be authenticated * * Defaults to `true` if an auth mechanism is registered. */ - authRequired?: boolean | 'optional' | 'try'; + authRequired?: boolean | 'optional'; /** * Defines xsrf protection requirements for a route: diff --git a/src/core/server/rendering/bootstrap/register_bootstrap_route.ts b/src/core/server/rendering/bootstrap/register_bootstrap_route.ts index 2c5274e89a221..5644b44f3508b 100644 --- a/src/core/server/rendering/bootstrap/register_bootstrap_route.ts +++ b/src/core/server/rendering/bootstrap/register_bootstrap_route.ts @@ -20,7 +20,7 @@ export const registerBootstrapRoute = ({ { path: '/bootstrap.js', options: { - authRequired: 'try', + authRequired: 'optional', tags: ['api'], }, validate: false, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 78b97d1c3f52e..a1a774e8721c8 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1966,7 +1966,7 @@ export interface RouteConfig { // @public export interface RouteConfigOptions { - authRequired?: boolean | 'optional' | 'try'; + authRequired?: boolean | 'optional'; body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; timeout?: { diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.ts b/src/dev/build/tasks/build_kibana_platform_plugins.ts index dfac3edf9847b..973d71043f028 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.ts +++ b/src/dev/build/tasks/build_kibana_platform_plugins.ts @@ -10,7 +10,7 @@ import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { lastValueFrom } from '@kbn/std'; -import { CiStatsMetrics } from '@kbn/dev-utils'; +import { CiStatsMetric } from '@kbn/dev-utils'; import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; import { Task, deleteAll, write, read } from '../lib'; @@ -32,11 +32,11 @@ export const BuildKibanaPlatformPlugins: Task = { await lastValueFrom(runOptimizer(config).pipe(logOptimizerState(log, config))); - const combinedMetrics: CiStatsMetrics = []; + const combinedMetrics: CiStatsMetric[] = []; const metricFilePaths: string[] = []; for (const bundle of config.bundles) { const path = Path.resolve(bundle.outputDir, 'metrics.json'); - const metrics: CiStatsMetrics = JSON.parse(await read(path)); + const metrics: CiStatsMetric[] = JSON.parse(await read(path)); combinedMetrics.push(...metrics); metricFilePaths.push(path); } diff --git a/src/dev/build/tasks/create_archives_task.ts b/src/dev/build/tasks/create_archives_task.ts index 5dc5ee07ed9ef..507c413067fb2 100644 --- a/src/dev/build/tasks/create_archives_task.ts +++ b/src/dev/build/tasks/create_archives_task.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import { promisify } from 'util'; -import { CiStatsMetrics } from '@kbn/dev-utils'; +import { CiStatsMetric } from '@kbn/dev-utils'; import { mkdirp, compressTar, compressZip, Task } from '../lib'; @@ -72,7 +72,7 @@ export const CreateArchives: Task = { } } - const metrics: CiStatsMetrics = []; + const metrics: CiStatsMetric[] = []; for (const { format, path, fileCount } of archives) { metrics.push({ group: `${build.isOss() ? 'oss ' : ''}distributable size`, diff --git a/src/legacy/server/config/complete.js b/src/legacy/server/config/complete.js deleted file mode 100644 index 5d3b2e55288bb..0000000000000 --- a/src/legacy/server/config/complete.js +++ /dev/null @@ -1,13 +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. - */ - -export default function (kbnServer, server) { - server.decorate('server', 'config', function () { - return kbnServer.config; - }); -} diff --git a/src/legacy/server/config/complete.test.js b/src/legacy/server/config/complete.test.js deleted file mode 100644 index be12414d77e7b..0000000000000 --- a/src/legacy/server/config/complete.test.js +++ /dev/null @@ -1,54 +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 completeMixin from './complete'; -import sinon from 'sinon'; - -describe('server/config completeMixin()', function () { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - const setup = (options = {}) => { - const { settings = {}, configValues = {}, disabledPluginSpecs = [], plugins = [] } = options; - - const server = { - decorate: sinon.stub(), - }; - - const config = { - get: sinon.stub().returns(configValues), - }; - - const kbnServer = { - newPlatform: {}, - settings, - server, - config, - disabledPluginSpecs, - plugins, - }; - - const callCompleteMixin = () => completeMixin(kbnServer, server, config); - - return { config, callCompleteMixin, server }; - }; - - describe('server decoration', () => { - it('adds a config() function to the server', async () => { - const { config, callCompleteMixin, server } = setup({ - settings: {}, - configValues: {}, - }); - - await callCompleteMixin(); - sinon.assert.calledOnce(server.decorate); - sinon.assert.calledWith(server.decorate, 'server', 'config', sinon.match.func); - expect(server.decorate.firstCall.args[2]()).toBe(config); - }); - }); -}); diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 34dd700aef414..55593d13d4687 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -15,8 +15,6 @@ import { Config } from './config'; import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; -import warningsMixin from './warnings'; -import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; /** @@ -66,10 +64,6 @@ export default class KbnServer { coreMixin, loggingMixin, - warningsMixin, - - // tell the config we are done loading plugins - configCompleteMixin, // setup routes that serve the @kbn/optimizer output optimizeMixin diff --git a/src/legacy/server/warnings/index.js b/src/legacy/server/warnings/index.js deleted file mode 100644 index d08b9c4219744..0000000000000 --- a/src/legacy/server/warnings/index.js +++ /dev/null @@ -1,19 +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. - */ - -export default function (kbnServer, server) { - process.on('warning', (warning) => { - // deprecation warnings do no reflect a current problem for - // the user and therefor should be filtered out. - if (warning.name === 'DeprecationWarning') { - return; - } - - server.log(['warning', 'process'], warning); - }); -} diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 898d8e809fca1..aeaa2f76816e4 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { extractDeprecationMessages } from '../../../lib/utils'; +import { extractWarningMessages } from '../../../lib/utils'; import { XJson } from '../../../../../es_ui_shared/public'; const { collapseLiteralStrings } = XJson; // @ts-ignore @@ -88,8 +88,8 @@ export function sendRequestToES(args: EsRequestArgs): Promise const warnings = xhr.getResponseHeader('warning'); if (warnings) { - const deprecationMessages = extractDeprecationMessages(warnings); - value = deprecationMessages.join('\n') + '\n' + value; + const warningMessages = extractWarningMessages(warnings); + value = warningMessages.join('\n') + '\n' + value; } if (isMultiRequest) { diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index 0aac2d01ad758..71b305807e61d 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -48,14 +48,14 @@ export function formatRequestBodyDoc(data: string[], indent: boolean) { }; } -export function extractDeprecationMessages(warnings: string) { +export function extractWarningMessages(warnings: string) { // pattern for valid warning header const re = /\d{3} [0-9a-zA-Z!#$%&'*+-.^_`|~]+ \"((?:\t| |!|[\x23-\x5b]|[\x5d-\x7e]|[\x80-\xff]|\\\\|\\")*)\"(?: \"[^"]*\")?/; // split on any comma that is followed by an even number of quotes return _.map(splitOnUnquotedCommaSpace(warnings), (warning) => { const match = re.exec(warning); // extract the actual warning if there was a match - return '#! Deprecation: ' + (match !== null ? unescape(match[1]) : warning); + return '#! ' + (match !== null ? unescape(match[1]) : warning); }); } diff --git a/src/plugins/console/public/lib/utils/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js index ff851bbea3c46..d7fc690e1bc24 100644 --- a/src/plugins/console/public/lib/utils/utils.test.js +++ b/src/plugins/console/public/lib/utils/utils.test.js @@ -11,51 +11,51 @@ import * as utils from '.'; describe('Utils class', () => { test('extract deprecation messages', function () { expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT"' ) - ).toEqual(['#! Deprecation: this is a warning']); + ).toEqual(['#! this is a warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning"' ) - ).toEqual(['#! Deprecation: this is a warning']); + ).toEqual(['#! this is a warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning" "Mon, 27 Feb 2017 14:52:14 GMT"' ) - ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); + ).toEqual(['#! this is a warning', '#! this is a second warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning"' ) - ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); + ).toEqual(['#! this is a warning', '#! this is a second warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma" "Mon, 27 Feb 2017 14:52:14 GMT"' ) - ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); + ).toEqual(['#! this is a warning, and it includes a comma']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma"' ) - ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); + ).toEqual(['#! this is a warning, and it includes a comma']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\"" "Mon, 27 Feb 2017 14:52:14 GMT"' ) ).toEqual([ - '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', + '#! this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', ]); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\""' ) ).toEqual([ - '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', + '#! this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', ]); }); diff --git a/src/plugins/data/common/search/aggs/metrics/__snapshots__/median.test.ts.snap b/src/plugins/data/common/search/aggs/metrics/__snapshots__/median.test.ts.snap new file mode 100644 index 0000000000000..efea69fd463df --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/__snapshots__/median.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggTypeMetricMedianProvider class supports scripted fields 1`] = ` +Object { + "median": Object { + "percentiles": Object { + "percents": Array [ + 50, + ], + "script": Object { + "lang": undefined, + "source": "return 456", + }, + }, + }, +} +`; diff --git a/src/plugins/data/common/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts index cfcdafd58d7ff..612bf22628eb1 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -100,4 +100,41 @@ describe('AggTypeMetricMedianProvider class', () => { } `); }); + + it('supports scripted fields', () => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: 'bytes', + scripted: true, + language: 'painless', + script: 'return 456', + }; + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: METRIC_TYPES.MEDIAN, + type: METRIC_TYPES.MEDIAN, + schema: 'metric', + params: { + field: 'bytes', + }, + }, + ], + { + typesRegistry, + } + ); + + expect(aggConfigs.toDsl()).toMatchSnapshot(); + }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/median.ts b/src/plugins/data/common/search/aggs/metrics/median.ts index 626669732a658..bad4c7baf173f 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.ts @@ -42,11 +42,8 @@ export const getMedianMetricAgg = () => { name: 'field', type: 'field', filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], - write(agg, output) { - output.params.field = agg.getParam('field').name; - output.params.percents = [50]; - }, }, + { name: 'percents', default: [50], shouldShow: () => false, serialize: () => undefined }, ], getValue(agg, bucket) { return bucket[agg.id].values['50.0']; diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts index c9f0a5bf24944..f9d703900fc04 100644 --- a/src/plugins/data/server/search/collectors/usage.ts +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { once } from 'lodash'; +import { once, debounce } from 'lodash'; import type { CoreSetup, Logger } from 'kibana/server'; -import { SavedObjectsErrorHelpers } from '../../../../../core/server'; -import type { IEsSearchResponse } from '../../../common'; +import type { IEsSearchResponse, ISearchOptions } from '../../../common'; +import { isCompleteResponse } from '../../../common'; +import { CollectedUsage } from './register'; const SAVED_OBJECT_ID = 'search-telemetry'; -const MAX_RETRY_COUNT = 3; export interface SearchUsage { trackError(): Promise; @@ -25,34 +25,52 @@ export function usageProvider(core: CoreSetup): SearchUsage { return coreStart.savedObjects.createInternalRepository(); }); - const trackSuccess = async (duration: number, retryCount = 0) => { - const repository = await getRepository(); - try { - await repository.incrementCounter(SAVED_OBJECT_ID, SAVED_OBJECT_ID, [ - { fieldName: 'successCount' }, - { - fieldName: 'totalDuration', - incrementBy: duration, - }, - ]); - } catch (e) { - if (SavedObjectsErrorHelpers.isConflictError(e) && retryCount < MAX_RETRY_COUNT) { - setTimeout(() => trackSuccess(duration, retryCount + 1), 1000); - } - } + const collectedUsage: CollectedUsage = { + successCount: 0, + errorCount: 0, + totalDuration: 0, }; - const trackError = async (retryCount = 0) => { - const repository = await getRepository(); - try { - await repository.incrementCounter(SAVED_OBJECT_ID, SAVED_OBJECT_ID, [ - { fieldName: 'errorCount' }, - ]); - } catch (e) { - if (SavedObjectsErrorHelpers.isConflictError(e) && retryCount < MAX_RETRY_COUNT) { - setTimeout(() => trackError(retryCount + 1), 1000); + // Instead of updating the search count every time a search completes, we update some in-memory + // counts and only update the saved object every ~5 seconds + const updateSearchUsage = debounce( + async () => { + const repository = await getRepository(); + const { successCount, errorCount, totalDuration } = collectedUsage; + const counterFields = Object.entries(collectedUsage) + .map(([fieldName, incrementBy]) => ({ fieldName, incrementBy })) + // Filter out any zero values because `incrementCounter` will still increment them + .filter(({ incrementBy }) => incrementBy > 0); + + try { + await repository.incrementCounter( + SAVED_OBJECT_ID, + SAVED_OBJECT_ID, + counterFields + ); + + // Since search requests may have completed while the saved object was being updated, we minus + // what was just updated in the saved object rather than resetting the values to 0 + collectedUsage.successCount -= successCount; + collectedUsage.errorCount -= errorCount; + collectedUsage.totalDuration -= totalDuration; + } catch (e) { + // We didn't reset the counters so we'll retry when the next search request completes } - } + }, + 5000, + { maxWait: 5000 } + ); + + const trackSuccess = (duration: number) => { + collectedUsage.successCount++; + collectedUsage.totalDuration += duration; + return updateSearchUsage(); + }; + + const trackError = () => { + collectedUsage.errorCount++; + return updateSearchUsage(); }; return { trackSuccess, trackError }; @@ -61,9 +79,14 @@ export function usageProvider(core: CoreSetup): SearchUsage { /** * Rxjs observer for easily doing `tap(searchUsageObserver(logger, usage))` in an rxjs chain. */ -export function searchUsageObserver(logger: Logger, usage?: SearchUsage) { +export function searchUsageObserver( + logger: Logger, + usage?: SearchUsage, + { isRestore }: ISearchOptions = {} +) { return { next(response: IEsSearchResponse) { + if (isRestore || !isCompleteResponse(response)) return; logger.debug(`trackSearchStatus:next ${response.rawResponse.took}`); usage?.trackSuccess(response.rawResponse.took); }, diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 4b6f1f9786958..cc81dce94c4ec 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -53,6 +53,6 @@ export const esSearchStrategyProvider = ( } }; - return from(search()).pipe(tap(searchUsageObserver(logger, usage))); + return from(search()).pipe(tap(searchUsageObserver(logger, usage, options))); }, }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c6eb10a3e51ae..0118906c181cc 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -1369,7 +1369,7 @@ export interface SearchUsage { // Warning: (ae-missing-release-tag) "searchUsageObserver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage): { +export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage, { isRestore }?: ISearchOptions): { next(response: IEsSearchResponse): void; error(): void; }; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx index 1f3e87e69fd4c..155fbc7fd31f5 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx @@ -17,7 +17,6 @@ import { KBN_FIELD_TYPES, ES_FIELD_TYPES, DataPublicPluginStart, - FieldFormat, } from 'src/plugins/data/public'; import { CoreStart } from 'src/core/public'; import { castEsToKbnFieldTypeName } from '../../../../data/public'; @@ -45,7 +44,6 @@ export interface FormatSelectEditorState { fieldTypeFormats: FieldTypeFormat[]; fieldFormatId?: string; fieldFormatParams?: { [key: string]: unknown }; - format: FieldFormat; kbnType: KBN_FIELD_TYPES; } @@ -81,67 +79,48 @@ export class FormatSelectEditor extends PureComponent< > { constructor(props: FormatSelectEditorProps) { super(props); - const { fieldFormats, esTypes, value } = props; + const { fieldFormats, esTypes } = props; const kbnType = castEsToKbnFieldTypeName(esTypes[0] || 'keyword'); - // get current formatter for field, provides default if none exists - const format = value?.id - ? fieldFormats.getInstance(value?.id, value?.params) - : fieldFormats.getDefaultInstance(kbnType, esTypes); - this.state = { fieldTypeFormats: getFieldTypeFormatsList( kbnType, fieldFormats.getDefaultType(kbnType, esTypes) as FieldFormatInstanceType, fieldFormats ), - format, kbnType, }; } - onFormatChange = (formatId: string, params?: any) => { - const { fieldTypeFormats } = this.state; - const { fieldFormats, uiSettings } = this.props; - - const FieldFormatClass = fieldFormats.getType( - formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id - ) as FieldFormatInstanceType; - - const newFormat = new FieldFormatClass(params, (key: string) => uiSettings.get(key)); - - this.setState( - { - fieldFormatId: formatId, - fieldFormatParams: params, - format: newFormat, - }, - () => { - this.props.onChange( - formatId - ? { - id: formatId, - params: params || {}, - } - : undefined - ); - } + onFormatChange = (formatId: string, params?: any) => + this.props.onChange( + formatId + ? { + id: formatId, + params: params || {}, + } + : undefined ); - }; + onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => { const { fieldFormatId } = this.state; this.onFormatChange(fieldFormatId as string, newParams); }; render() { - const { fieldFormatEditors, onError, value } = this.props; + const { fieldFormatEditors, onError, value, fieldFormats, esTypes } = this.props; const fieldFormatId = value?.id; const fieldFormatParams = value?.params; const { kbnType } = this.state; - const { fieldTypeFormats, format } = this.state; + const { fieldTypeFormats } = this.state; const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title; + // get current formatter for field, provides default if none exists + const format = value?.id + ? fieldFormats.getInstance(value?.id, value?.params) + : fieldFormats.getDefaultInstance(kbnType, esTypes); + const label = defaultFormat ? ( ) => SavedObject; + /** @deprecated */ settings: { + /** @deprecated */ getPerPage: () => number; + /** @deprecated */ getListingLimit: () => number; }; } diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index e476d62a0e793..07f6336dac52c 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -66,6 +66,7 @@ export interface SaveModalState { const generateId = htmlIdGenerator(); +/** @deprecated */ export class SavedObjectSaveModal extends React.Component { private warning = React.createRef(); public readonly state = { diff --git a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts index f1de1933ac43b..10872b5d9cd1a 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object_loader.ts @@ -22,6 +22,7 @@ export interface SavedObjectLoaderFindOptions { } /** + * @deprecated * The SavedObjectLoader class provides some convenience functions * to load and save one kind of saved objects (specified in the constructor). * diff --git a/src/plugins/saved_objects/public/types.ts b/src/plugins/saved_objects/public/types.ts index b0bc459cd299b..ca44c56319954 100644 --- a/src/plugins/saved_objects/public/types.ts +++ b/src/plugins/saved_objects/public/types.ts @@ -21,6 +21,7 @@ import { SearchSourceFields, } from '../../data/public'; +/** @deprecated */ export interface SavedObject { _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; _source: Record; diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index d36cea80bffff..61198518dc333 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -8,7 +8,7 @@ import { get } from 'lodash'; import PropTypes from 'prop-types'; -import React, { useContext, useCallback } from 'react'; +import React, { useContext, useCallback, useEffect } from 'react'; import { htmlIdGenerator, EuiFieldText, @@ -123,7 +123,9 @@ export const IndexPattern = ({ ); const isTimeSeries = model.type === PANEL_TYPES.TIMESERIES; - updateControlValidity(intervalName, intervalValidation.isValid); + useEffect(() => { + updateControlValidity(intervalName, intervalValidation.isValid); + }, [intervalName, intervalValidation.isValid, updateControlValidity]); return (
diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js deleted file mode 100644 index 2e6e97f868fd9..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js +++ /dev/null @@ -1,85 +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 PropTypes from 'prop-types'; -import React, { useState, useEffect } from 'react'; -import { TimeseriesPanelConfig as timeseries } from './panel_config/timeseries'; -import { MetricPanelConfig as metric } from './panel_config/metric'; -import { TopNPanelConfig as topN } from './panel_config/top_n'; -import { TablePanelConfig as table } from './panel_config/table'; -import { GaugePanelConfig as gauge } from './panel_config/gauge'; -import { MarkdownPanelConfig as markdown } from './panel_config/markdown'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { FormValidationContext } from '../contexts/form_validation_context'; -import { VisDataContext } from '../contexts/vis_data_context'; - -const types = { - timeseries, - table, - metric, - top_n: topN, - gauge, - markdown, -}; - -const checkModelValidity = (validationResults) => - Boolean(Object.values(validationResults).every((isValid) => isValid)); - -export function PanelConfig(props) { - const { model } = props; - const Component = types[model.type]; - const [formValidationResults] = useState({}); - const [visData, setVisData] = useState({}); - - useEffect(() => { - model.isModelInvalid = !checkModelValidity(formValidationResults); - }); - - useEffect(() => { - const visDataSubscription = props.visData$.subscribe((visData = {}) => setVisData(visData)); - - return function cleanup() { - visDataSubscription.unsubscribe(); - }; - }, [model.id, props.visData$]); - - const updateControlValidity = (controlKey, isControlValid) => { - formValidationResults[controlKey] = isControlValid; - }; - - if (Component) { - return ( - - -
- -
-
-
- ); - } - - return ( -
- -
- ); -} - -PanelConfig.propTypes = { - fields: PropTypes.object, - model: PropTypes.object, - onChange: PropTypes.func, - visData$: PropTypes.object, - getConfig: PropTypes.func, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx new file mode 100644 index 0000000000000..b1eed37986e16 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx @@ -0,0 +1,79 @@ +/* + * 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, useEffect, useCallback, useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Observable } from 'rxjs'; + +import { IUiSettingsClient } from 'kibana/public'; +import { TimeseriesVisData } from '../../../common/types'; +import { FormValidationContext } from '../contexts/form_validation_context'; +import { VisDataContext } from '../contexts/vis_data_context'; +import { panelConfigTypes } from './panel_config/index'; +import { TimeseriesVisParams } from '../../types'; +import { VisFields } from '../lib/fetch_fields'; + +interface FormValidationResults { + [key: string]: boolean; +} + +interface PanelConfigProps { + fields?: VisFields; + model: TimeseriesVisParams; + visData$: Observable; + getConfig: IUiSettingsClient['get']; + onChange: (partialModel: Partial) => void; +} + +const checkModelValidity = (validationResults: FormValidationResults) => + Object.values(validationResults).every((isValid) => isValid); + +export function PanelConfig(props: PanelConfigProps) { + const { model, onChange } = props; + const Component = panelConfigTypes[model.type]; + const formValidationResults = useRef({}); + const [visData, setVisData] = useState({} as TimeseriesVisData); + + useEffect(() => { + const visDataSubscription = props.visData$.subscribe((data = {} as TimeseriesVisData) => + setVisData(data) + ); + + return () => visDataSubscription.unsubscribe(); + }, [model.id, props.visData$]); + + const updateControlValidity = useCallback( + (controlKey: string, isControlValid: boolean) => { + formValidationResults.current[controlKey] = isControlValid; + onChange({ isModelInvalid: !checkModelValidity(formValidationResults.current) }); + }, + [onChange] + ); + + if (Component) { + return ( + + +
+ +
+
+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts b/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts new file mode 100644 index 0000000000000..f50ecca1894aa --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +// these are not typed yet +// @ts-expect-error +import { TimeseriesPanelConfig as timeseries } from './timeseries'; +// @ts-expect-error +import { MetricPanelConfig as metric } from './metric'; +// @ts-expect-error +import { TopNPanelConfig as topN } from './top_n'; +// @ts-expect-error +import { TablePanelConfig as table } from './table'; +// @ts-expect-error +import { GaugePanelConfig as gauge } from './gauge'; +// @ts-expect-error +import { MarkdownPanelConfig as markdown } from './markdown'; + +export const panelConfigTypes = { + timeseries, + table, + metric, + top_n: topN, + gauge, + markdown, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index fe09ae253f8a8..9c8944a2e6e62 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -15,7 +15,7 @@ import { PersistedState } from 'src/plugins/visualizations/public'; // @ts-expect-error import { ErrorComponent } from './error'; import { TimeseriesVisTypes } from './vis_types'; -import { TimeseriesVisParams } from '../../metrics_fn'; +import { TimeseriesVisParams } from '../../types'; import { TimeseriesVisData } from '../../../common/types'; interface TimeseriesVisualizationProps { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx similarity index 50% rename from src/plugins/vis_type_timeseries/public/application/components/vis_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index 11586628ea005..ffef437358c3d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -6,50 +6,74 @@ * Side Public License, v 1. */ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; +import { EventEmitter } from 'events'; + +import { IUiSettingsClient } from 'kibana/public'; +import { TimeRange } from 'src/plugins/data/public'; +import { + PersistedState, + Vis, + VisualizeEmbeddableContract, +} from 'src/plugins/visualizations/public'; +import { TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; +import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; +import { Storage } from '../../../../../plugins/kibana_utils/public'; + +// @ts-expect-error import { VisEditorVisualization } from './vis_editor_visualization'; -import { VisPicker } from './vis_picker'; import { PanelConfig } from './panel_config'; -import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../common/extract_index_patterns'; -import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; - -import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; -import { Storage } from '../../../../../plugins/kibana_utils/public'; +import { VisPicker } from './vis_picker'; +import { fetchFields, VisFields } from '../lib/fetch_fields'; +import { getDataStart, getCoreStart } from '../../services'; +import { TimeseriesVisParams } from '../../types'; const VIS_STATE_DEBOUNCE_DELAY = 200; const APP_NAME = 'VisEditor'; -export class VisEditor extends Component { - constructor(props) { - super(props); - this.localStorage = new Storage(window.localStorage); - this.state = {}; +export interface TimeseriesEditorProps { + config: IUiSettingsClient; + embeddableHandler: VisualizeEmbeddableContract; + eventEmitter: EventEmitter; + timeRange: TimeRange; + uiState: PersistedState; + vis: Vis; +} - this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); - this.visData$ = this.visDataSubject.asObservable().pipe(share()); +interface TimeseriesEditorState { + autoApply: boolean; + dirty: boolean; + extractedIndexPatterns: string[]; + model: TimeseriesVisParams; + visFields?: VisFields; +} + +export class VisEditor extends Component { + private abortControllerFetchFields?: AbortController; + private localStorage: Storage; + private visDataSubject: Rx.BehaviorSubject; + private visData$: Rx.Observable; - // In new_platform, this context should be populated with - // core dependencies required by React components downstream. - this.coreContext = { - appName: APP_NAME, - uiSettings: getUISettings(), - savedObjectsClient: getSavedObjectsClient(), - store: this.localStorage, + constructor(props: TimeseriesEditorProps) { + super(props); + this.localStorage = new Storage(window.localStorage); + this.state = { + autoApply: true, + dirty: false, + model: this.props.vis.params, + extractedIndexPatterns: [''], }; - } - get uiState() { - return this.props.vis.uiState; + this.visDataSubject = new Rx.BehaviorSubject(undefined); + this.visData$ = this.visDataSubject.asObservable().pipe(share()); } - getConfig = (...args) => { - return this.props.config.get(...args); + getConfig = (key: string) => { + return this.props.config.get(key); }; updateVisState = debounce(() => { @@ -73,16 +97,14 @@ export class VisEditor extends Component { } }, VIS_STATE_DEBOUNCE_DELAY); - abortableFetchFields = (extractedIndexPatterns) => { - if (this.abortControllerFetchFields) { - this.abortControllerFetchFields.abort(); - } + abortableFetchFields = (extractedIndexPatterns: string[]) => { + this.abortControllerFetchFields?.abort(); this.abortControllerFetchFields = new AbortController(); return fetchFields(extractedIndexPatterns, this.abortControllerFetchFields.signal); }; - handleChange = (partialModel) => { + handleChange = (partialModel: Partial) => { if (isEmpty(partialModel)) { return; } @@ -117,64 +139,62 @@ export class VisEditor extends Component { this.setState({ dirty: false }); }; - handleAutoApplyToggle = (event) => { + handleAutoApplyToggle = (event: React.ChangeEvent) => { this.setState({ autoApply: event.target.checked }); }; - onDataChange = ({ visData }) => { + onDataChange = ({ visData }: { visData: TimeseriesVisData }) => { this.visDataSubject.next(visData); }; render() { - const { model } = this.state; - - if (model) { - //TODO: Remove CoreStartContextProvider, KibanaContextProvider should be raised to the top of the plugin. - return ( - -
-
- -
- +
+
+ +
+ +
+ -
- - - -
- - ); - } - - return null; +
+ + ); } componentDidMount() { @@ -182,11 +202,12 @@ export class VisEditor extends Component { dataStart.indexPatterns.getDefault().then(async (index) => { const defaultIndexTitle = index?.title ?? ''; - const indexPatterns = extractIndexPatterns(this.props.visParams, defaultIndexTitle); + const indexPatterns = extractIndexPatterns(this.props.vis.params, defaultIndexTitle); + const visFields = await fetchFields(indexPatterns); - this.setState({ + this.setState((state) => ({ model: { - ...this.props.visParams, + ...state.model, /** @legacy * please use IndexPatterns service instead * **/ @@ -196,11 +217,8 @@ export class VisEditor extends Component { * **/ default_timefield: index?.timeFieldName ?? '', }, - dirty: false, - autoApply: true, - visFields: await fetchFields(indexPatterns), - extractedIndexPatterns: [''], - }); + visFields, + })); }); this.props.eventEmitter.on('updateEditor', this.updateModel); @@ -212,19 +230,6 @@ export class VisEditor extends Component { } } -VisEditor.defaultProps = { - visData: {}, -}; - -VisEditor.propTypes = { - vis: PropTypes.object, - visData: PropTypes.object, - renderComplete: PropTypes.func, - config: PropTypes.object, - timeRange: PropTypes.object, - appState: PropTypes.object, -}; - // default export required for React.Lazy // eslint-disable-next-line import/no-default-export export { VisEditor as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx index 2cbd88cefcf0a..8b54349c495f4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx @@ -8,11 +8,11 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; +import type { TimeseriesEditorProps } from './vis_editor'; -// @ts-ignore const VisEditorComponent = lazy(() => import('./vis_editor')); -export const VisEditor = (props: any) => ( +export const VisEditor = (props: TimeseriesEditorProps) => ( }> diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js deleted file mode 100644 index fab1de45156cc..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js +++ /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 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 PropTypes from 'prop-types'; -import React from 'react'; -import { EuiTabs, EuiTab } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; -import { PANEL_TYPES } from '../../../common/panel_types'; - -function VisPickerItem(props) { - const { label, type, selected } = props; - const itemClassName = 'tvbVisPickerItem'; - - return ( - props.onClick(type)} - data-test-subj={`${type}TsvbTypeBtn`} - > - {label} - - ); -} - -VisPickerItem.propTypes = { - label: PropTypes.string, - onClick: PropTypes.func, - type: PropTypes.string, - selected: PropTypes.bool, -}; - -export const VisPicker = injectI18n(function (props) { - const handleChange = (type) => { - props.onChange({ type }); - }; - - const { model, intl } = props; - const tabs = [ - { - type: PANEL_TYPES.TIMESERIES, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.timeSeriesLabel', - defaultMessage: 'Time Series', - }), - }, - { - type: PANEL_TYPES.METRIC, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.metricLabel', - defaultMessage: 'Metric', - }), - }, - { - type: PANEL_TYPES.TOP_N, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.topNLabel', - defaultMessage: 'Top N', - }), - }, - { - type: PANEL_TYPES.GAUGE, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.gaugeLabel', - defaultMessage: 'Gauge', - }), - }, - { type: PANEL_TYPES.MARKDOWN, label: 'Markdown' }, - { - type: PANEL_TYPES.TABLE, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.tableLabel', - defaultMessage: 'Table', - }), - }, - ].map((item) => { - return ( - - ); - }); - - return {tabs}; -}); - -VisPicker.propTypes = { - model: PropTypes.object, - onChange: PropTypes.func, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx new file mode 100644 index 0000000000000..74ef710b1656f --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx @@ -0,0 +1,70 @@ +/* + * 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 from 'react'; +import { EuiTabs, EuiTab } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { PANEL_TYPES } from '../../../common/panel_types'; +import { TimeseriesVisParams } from '../../types'; + +const tabs = [ + { + type: PANEL_TYPES.TIMESERIES, + label: i18n.translate('visTypeTimeseries.visPicker.timeSeriesLabel', { + defaultMessage: 'Time Series', + }), + }, + { + type: PANEL_TYPES.METRIC, + label: i18n.translate('visTypeTimeseries.visPicker.metricLabel', { + defaultMessage: 'Metric', + }), + }, + { + type: PANEL_TYPES.TOP_N, + label: i18n.translate('visTypeTimeseries.visPicker.topNLabel', { + defaultMessage: 'Top N', + }), + }, + { + type: PANEL_TYPES.GAUGE, + label: i18n.translate('visTypeTimeseries.visPicker.gaugeLabel', { + defaultMessage: 'Gauge', + }), + }, + { type: PANEL_TYPES.MARKDOWN, label: 'Markdown' }, + { + type: PANEL_TYPES.TABLE, + label: i18n.translate('visTypeTimeseries.visPicker.tableLabel', { + defaultMessage: 'Table', + }), + }, +]; + +interface VisPickerProps { + onChange: (partialModel: Partial) => void; + currentVisType: TimeseriesVisParams['type']; +} + +export const VisPicker = ({ onChange, currentVisType }: VisPickerProps) => { + return ( + + {tabs.map(({ label, type }) => ( + onChange({ type })} + data-test-subj={`${type}TsvbTypeBtn`} + > + {label} + + ))} + + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 8b638e1f41131..150a3a716a879 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -11,7 +11,7 @@ import React, { lazy } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { PersistedState } from 'src/plugins/visualizations/public'; -import { TimeseriesVisParams } from '../../../metrics_fn'; +import { TimeseriesVisParams } from '../../../types'; import { TimeseriesVisData } from '../../../../common/types'; /** diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.ts diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts similarity index 68% rename from src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts index 1a953be530eb7..c03202b5fb4e2 100644 --- a/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js +++ b/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts @@ -7,5 +7,6 @@ */ import React from 'react'; +import { TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; -export const VisDataContext = React.createContext({}); +export const VisDataContext = React.createContext({} as TimeseriesVisData); diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.tsx similarity index 59% rename from src/plugins/vis_type_timeseries/public/application/editor_controller.js rename to src/plugins/vis_type_timeseries/public/application/editor_controller.tsx index f33382982abfd..e2ad9cf90bb30 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.tsx @@ -8,37 +8,36 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { EventEmitter } from 'events'; + +import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { IEditorController, EditorRenderProps } from 'src/plugins/visualize/public'; import { getUISettings, getI18n } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; +import { TimeseriesVisParams } from '../types'; export const TSVB_EDITOR_NAME = 'tsvbEditor'; -export class EditorController { - constructor(el, vis, eventEmitter, embeddableHandler) { - this.el = el; - - this.embeddableHandler = embeddableHandler; - this.eventEmitter = eventEmitter; - - this.state = { - vis: vis, - }; - } +export class EditorController implements IEditorController { + constructor( + private el: HTMLElement, + private vis: Vis, + private eventEmitter: EventEmitter, + private embeddableHandler: VisualizeEmbeddableContract + ) {} - async render(params) { + render({ timeRange, uiState }: EditorRenderProps) { const I18nContext = getI18n().Context; render( {}} - appState={params.appState} + vis={this.vis} + timeRange={timeRange} embeddableHandler={this.embeddableHandler} eventEmitter={this.eventEmitter} + uiState={uiState} /> , this.el diff --git a/src/plugins/vis_type_timeseries/public/application/index.ts b/src/plugins/vis_type_timeseries/public/application/index.ts index de34e91fd4798..fcc0c592b1ef5 100644 --- a/src/plugins/vis_type_timeseries/public/application/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -// @ts-ignore export { EditorController, TSVB_EDITOR_NAME } from './editor_controller'; export * from './lib'; diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts index 4b41747f23c79..088930f90a765 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts +++ b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts @@ -11,10 +11,12 @@ import { getCoreStart, getDataStart } from '../../services'; import { ROUTES } from '../../../common/constants'; import { SanitizedFieldType } from '../../../common/types'; +export type VisFields = Record; + export async function fetchFields( indexes: string[] = [], signal?: AbortSignal -): Promise> { +): Promise { const patterns = Array.isArray(indexes) ? indexes : [indexes]; const coreStart = getCoreStart(); const dataStart = getDataStart(); @@ -32,7 +34,7 @@ export async function fetchFields( }) ); - const fields: Record = patterns.reduce( + const fields: VisFields = patterns.reduce( (cumulatedFields, currentPattern, index) => ({ ...cumulatedFields, [currentPattern]: indexFields[index], diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 8ad42dea407e2..aec5013f4eb80 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { KibanaContext } from '../../data/public'; import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; -import { PanelSchema, TimeseriesVisData } from '../common/types'; +import { TimeseriesVisData } from '../common/types'; import { metricsRequestHandler } from './request_handler'; +import { TimeseriesVisParams } from './types'; type Input = KibanaContext | null; type Output = Promise>; @@ -21,8 +22,6 @@ interface Arguments { uiState: string; } -export type TimeseriesVisParams = PanelSchema; - export interface TimeseriesRenderValue { visData: TimeseriesVisData | {}; visParams: TimeseriesVisParams; diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index d0526f7e1d886..bf3779674b6ea 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -11,7 +11,7 @@ import { KibanaContext } from '../../data/public'; import { getTimezone, validateInterval } from './application'; import { getUISettings, getDataStart, getCoreStart } from './services'; import { MAX_BUCKETS_SETTING, ROUTES } from '../common/constants'; -import { TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesVisParams } from './types'; import { TimeseriesVisData } from '../common/types'; interface MetricsRequestHandlerParams { diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx index 1f992cb2db511..06c5d20f08a7c 100644 --- a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx @@ -13,8 +13,9 @@ import { IUiSettingsClient } from 'kibana/public'; import type { PersistedState } from '../../visualizations/public'; import { VisualizationContainer } from '../../visualizations/public'; import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; -import { TimeseriesRenderValue, TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesRenderValue } from './metrics_fn'; import { TimeseriesVisData } from '../common/types'; +import { TimeseriesVisParams } from './types'; const TimeseriesVisualization = lazy( () => import('./application/components/timeseries_visualization') diff --git a/src/plugins/vis_type_timeseries/public/to_ast.ts b/src/plugins/vis_type_timeseries/public/to_ast.ts index ebceab333d422..90d57218da28c 100644 --- a/src/plugins/vis_type_timeseries/public/to_ast.ts +++ b/src/plugins/vis_type_timeseries/public/to_ast.ts @@ -8,7 +8,8 @@ import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { Vis } from '../../visualizations/public'; -import { TimeseriesExpressionFunctionDefinition, TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesExpressionFunctionDefinition } from './metrics_fn'; +import { TimeseriesVisParams } from './types'; export const toExpressionAst = (vis: Vis) => { const timeseries = buildExpressionFunction('tsvb', { diff --git a/src/plugins/vis_type_timeseries/public/types.ts b/src/plugins/vis_type_timeseries/public/types.ts index 5c6dcfdcf6599..2986ba6d45f83 100644 --- a/src/plugins/vis_type_timeseries/public/types.ts +++ b/src/plugins/vis_type_timeseries/public/types.ts @@ -8,6 +8,7 @@ import React from 'react'; import { EuiDraggable } from '@elastic/eui'; +import { PanelSchema } from '../common/types'; type PropsOf = T extends React.ComponentType ? ComponentProps : never; type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any @@ -16,3 +17,5 @@ type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: a export type DragHandleProps = FirstArgumentOf< Exclude['children'], React.ReactElement> >['dragHandleProps']; + +export type TimeseriesVisParams = PanelSchema; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 68c1bb3e0c87d..4dd14a6a4a5a1 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -39,12 +39,12 @@ export interface SerializedVisData { savedSearchId?: string; } -export interface SerializedVis { +export interface SerializedVis { id?: string; title: string; description?: string; type: string; - params: VisParams; + params: T; uiState?: any; data: SerializedVisData; } @@ -80,7 +80,7 @@ export class Vis { public readonly uiState: PersistedState; - constructor(visType: string, visState: SerializedVis = {} as any) { + constructor(visType: string, visState: SerializedVis = {} as any) { this.type = this.getType(visType); this.params = this.getParams(visState.params); this.uiState = new PersistedState(visState.uiState); @@ -154,9 +154,9 @@ export class Vis { } } - clone() { + clone(): Vis { const { data, ...restOfSerialized } = this.serialize(); - const vis = new Vis(this.type.name, restOfSerialized as any); + const vis = new Vis(this.type.name, restOfSerialized as any); vis.setState({ ...restOfSerialized, data: {} }); const aggs = this.data.indexPattern ? getAggs().createAggConfigs(this.data.indexPattern, data.aggs) @@ -175,7 +175,7 @@ export class Vis { title: this.title, description: this.description, type: this.type.name, - params: cloneDeep(this.params) as any, + params: cloneDeep(this.params), uiState: this.uiState.toJSON(), data: { aggs: aggs as any, diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 67c3d22d95426..da18b3b97a522 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -15,6 +15,7 @@ import { VisualizeEmbeddableContract, VisSavedObject, PersistedState, + VisParams, } from 'src/plugins/visualizations/public'; import { CoreStart, @@ -114,9 +115,9 @@ export interface ByValueVisInstance { export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; -export type VisEditorConstructor = new ( +export type VisEditorConstructor = new ( element: HTMLElement, - vis: Vis, + vis: Vis, eventEmitter: EventEmitter, embeddableHandler: VisualizeEmbeddableContract ) => IEditorController; diff --git a/src/plugins/visualize/public/vis_editors_registry.ts b/src/plugins/visualize/public/vis_editors_registry.ts index 42dd89c2d9042..2cb018e78954b 100644 --- a/src/plugins/visualize/public/vis_editors_registry.ts +++ b/src/plugins/visualize/public/vis_editors_registry.ts @@ -11,13 +11,13 @@ import { VisEditorConstructor } from './application/types'; const DEFAULT_NAME = 'default'; export const createVisEditorsRegistry = () => { - const map = new Map(); + const map = new Map>(); return { registerDefault: (editor: VisEditorConstructor) => { map.set(DEFAULT_NAME, editor); }, - register: (name: string, editor: VisEditorConstructor) => { + register: (name: string, editor: VisEditorConstructor) => { if (name) { map.set(name, editor); } diff --git a/test/functional/apps/management/_field_formatter.js b/test/functional/apps/management/_field_formatter.js new file mode 100644 index 0000000000000..49b6c162caa41 --- /dev/null +++ b/test/functional/apps/management/_field_formatter.js @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +export default function ({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['settings']); + const testSubjects = getService('testSubjects'); + + describe('field formatter', function () { + this.tags(['skipFirefox']); + + before(async function () { + await browser.setWindowSize(1200, 800); + await esArchiver.load('discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.update({}); + }); + + after(async function afterAll() { + await PageObjects.settings.navigateTo(); + await esArchiver.emptyKibanaIndex(); + }); + + describe('set and change field formatter', function describeIndexTests() { + // addresses https://github.com/elastic/kibana/issues/93349 + it('can change format more than once', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.settings.clickAddField(); + await PageObjects.settings.setFieldType('Long'); + const formatRow = await testSubjects.find('formatRow'); + const formatRowToggle = ( + await formatRow.findAllByCssSelector('[data-test-subj="toggle"]') + )[0]; + + await formatRowToggle.click(); + await PageObjects.settings.setFieldFormat('duration'); + await PageObjects.settings.setFieldFormat('bytes'); + await PageObjects.settings.setFieldFormat('duration'); + await testSubjects.click('euiFlyoutCloseButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); + }); + }); + }); +} diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index fcb4e49dc7548..ac0df0aa90c4d 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -34,6 +34,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_index_patterns_empty')); loadTestFile(require.resolve('./_scripted_fields')); loadTestFile(require.resolve('./_runtime_fields')); + loadTestFile(require.resolve('./_field_formatter')); }); describe('', function () { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index d4c56bc48c139..da676fd649293 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -19,5 +19,7 @@ export function iconForNode(node: cytoscape.NodeSingular) { const subtype = node.data(SPAN_SUBTYPE); const type = node.data(SPAN_TYPE); - return agentName ? getAgentIcon(agentName) : getSpanIcon(type, subtype); + return agentName + ? getAgentIcon(agentName, false) + : getSpanIcon(type, subtype); } diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx index 4c14a3249fded..f7495d3e51671 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx @@ -44,7 +44,7 @@ export function IconPopover({ ownFocus={false} button={ - + } isOpen={isOpen} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx index dbf4b65deb3b3..6027e8b1d07c5 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx @@ -19,6 +19,7 @@ import { } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; import { ServiceIcons } from './'; +import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -60,7 +61,9 @@ describe('ServiceIcons', () => { }); const { getByTestId, queryAllByTestId } = render( - + + + ); expect(getByTestId('loading')).toBeInTheDocument(); @@ -77,7 +80,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -96,7 +101,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -116,7 +123,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -137,7 +146,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -180,7 +191,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -216,7 +229,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId, getByText } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx index bb68f74e9846e..6f9c82200fb60 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx @@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ReactChild, useState } from 'react'; +import { useTheme } from '../../../../hooks/use_theme'; import { ContainerType } from '../../../../../common/service_metadata'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; @@ -63,6 +64,8 @@ export function ServiceIcons({ serviceName }: Props) { setSelectedIconPopover, ] = useState(); + const theme = useTheme(); + const { data: icons, status: iconsFetchStatus } = useFetcher( (callApmApi) => { if (serviceName && start && end) { @@ -103,7 +106,7 @@ export function ServiceIcons({ serviceName }: Props) { const popoverItems: PopoverItem[] = [ { key: 'service', - icon: getAgentIcon(icons?.agentName) || 'node', + icon: getAgentIcon(icons?.agentName, theme.darkMode) || 'node', isVisible: !!icons?.agentName, title: i18n.translate('xpack.apm.serviceIcons.service', { defaultMessage: 'Service', diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts index 00282c681cbcd..f916292b7f080 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts @@ -22,7 +22,10 @@ import phpIcon from './icons/php.svg'; import pythonIcon from './icons/python.svg'; import rubyIcon from './icons/ruby.svg'; import rumJsIcon from './icons/rumjs.svg'; +import darkPhpIcon from './icons/php_dark.svg'; +import darkRumJsIcon from './icons/rumjs_dark.svg'; import rustIcon from './icons/rust.svg'; +import darkRustIcon from './icons/rust_dark.svg'; const agentIcons: { [key: string]: string } = { dotnet: dotNetIcon, @@ -39,6 +42,13 @@ const agentIcons: { [key: string]: string } = { rust: rustIcon, }; +const darkAgentIcons: { [key: string]: string } = { + ...agentIcons, + php: darkPhpIcon, + rum: darkRumJsIcon, + rust: darkRustIcon, +}; + // This only needs to be exported for testing purposes, since we stub the SVG // import values in test. export function getAgentIconKey(agentName: string) { @@ -66,7 +76,13 @@ export function getAgentIconKey(agentName: string) { } } -export function getAgentIcon(agentName?: string) { +export function getAgentIcon( + agentName: string | undefined, + isDarkMode: boolean +) { const key = agentName && getAgentIconKey(agentName); - return (key && agentIcons[key]) ?? defaultIcon; + if (!key) { + return defaultIcon; + } + return (isDarkMode ? darkAgentIcons[key] : agentIcons[key]) ?? defaultIcon; } diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg index c8af5dc331269..9fc450854d40f 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg @@ -1,18 +1,9 @@ - - - - - - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg new file mode 100644 index 0000000000000..e62cf4580198e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg index 87043159ed8c3..4b8cb916b1212 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg @@ -1,3 +1,8 @@ - - - + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg new file mode 100644 index 0000000000000..9cb79b0965451 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg new file mode 100644 index 0000000000000..e12620f756f52 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx index 25abaac82b0a0..f91eb49717782 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx @@ -6,19 +6,19 @@ */ import React from 'react'; +import { EuiIcon } from '@elastic/eui'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { useTheme } from '../../../hooks/use_theme'; import { getAgentIcon } from './get_agent_icon'; +import { useTheme } from '../../../hooks/use_theme'; interface Props { agentName: AgentName; } export function AgentIcon(props: Props) { - const theme = useTheme(); const { agentName } = props; - const size = theme.eui.euiIconSizes.large; - const icon = getAgentIcon(agentName); + const theme = useTheme(); + const icon = getAgentIcon(agentName, theme.darkMode); - return {agentName}; + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx index db21d781e9eba..05e4067f522a1 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useTheme } from '../../../hooks/use_theme'; +import { EuiIcon } from '@elastic/eui'; import { getSpanIcon } from './get_span_icon'; interface Props { @@ -15,9 +15,7 @@ interface Props { } export function SpanIcon({ type, subType }: Props) { - const theme = useTheme(); - const size = theme.eui.euiIconSizes.large; const icon = getSpanIcon(type, subType); - return {type; + return ; } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts index 52de4c68e512d..e662d51500333 100644 --- a/x-pack/plugins/cloud/public/user_menu_links.ts +++ b/x-pack/plugins/cloud/public/user_menu_links.ts @@ -16,11 +16,12 @@ export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => if (resetPasswordUrl) { userMenuLinks.push({ label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { - defaultMessage: 'Cloud profile', + defaultMessage: 'Profile', }), - iconType: 'logoCloud', + iconType: 'user', href: resetPasswordUrl, order: 100, + setAsProfile: true, }); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index a4ce724fdb097..f3a67c0d10389 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -229,8 +229,7 @@ export const EngineNav: React.FC = () => { )} {canManageEngineResultSettings && ( {RESULT_SETTINGS_TITLE} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index e6b829a43dcc1..7355ee148814c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -20,6 +20,7 @@ import { AnalyticsRouter } from '../analytics'; import { CurationsRouter } from '../curations'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; +import { ResultSettings } from '../result_settings'; import { EngineRouter } from './engine_router'; @@ -111,4 +112,11 @@ describe('EngineRouter', () => { expect(wrapper.find(RelevanceTuning)).toHaveLength(1); }); + + it('renders a result settings view', () => { + setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); + const wrapper = shallow(); + + expect(wrapper.find(ResultSettings)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 305bdf74ae501..8eb50626fcb2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -29,7 +29,7 @@ import { ENGINE_RELEVANCE_TUNING_PATH, // ENGINE_SYNONYMS_PATH, ENGINE_CURATIONS_PATH, - // ENGINE_RESULT_SETTINGS_PATH, + ENGINE_RESULT_SETTINGS_PATH, // ENGINE_SEARCH_UI_PATH, // ENGINE_API_LOGS_PATH, } from '../../routes'; @@ -41,6 +41,8 @@ import { EngineOverview } from '../engine_overview'; import { ENGINES_TITLE } from '../engines'; import { RelevanceTuning } from '../relevance_tuning'; +import { ResultSettings } from '../result_settings'; + import { EngineLogic } from './'; export const EngineRouter: React.FC = () => { @@ -54,7 +56,7 @@ export const EngineRouter: React.FC = () => { canManageEngineRelevanceTuning, // canManageEngineSynonyms, canManageEngineCurations, - // canManageEngineResultSettings, + canManageEngineResultSettings, // canManageEngineSearchUi, // canViewEngineApiLogs, }, @@ -108,6 +110,11 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineResultSettings && ( + + + + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index e0c5823503445..3b9b6e6c6a778 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -54,7 +54,7 @@ export const EnginesTable: React.FC = ({ const { navigateToUrl } = useValues(KibanaLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); - const generteEncodedEnginePath = (engineName: string) => + const generateEncodedEnginePath = (engineName: string) => generateEncodedPath(ENGINE_PATH, { engineName }); const sendEngineTableLinkClickTelemetry = () => sendAppSearchTelemetry({ @@ -71,7 +71,7 @@ export const EnginesTable: React.FC = ({ render: (name: string) => ( {name} @@ -159,7 +159,7 @@ export const EnginesTable: React.FC = ({ icon: 'eye', onClick: (engineDetails) => { sendEngineTableLinkClickTelemetry(); - navigateToUrl(generteEncodedEnginePath(engineDetails.name)); + navigateToUrl(generateEncodedEnginePath(engineDetails.name)); }, }, { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index f76ad78c847d1..3f72199d12805 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiSpacer, @@ -18,7 +18,7 @@ import { import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Schema } from '../../../shared/types'; -import { Result } from '../result/result'; +import { Result } from '../result'; export const Library: React.FC = () => { const props = { @@ -70,6 +70,16 @@ export const Library: React.FC = () => { length: 'number', }; + const [isActionButtonFilled, setIsActionButtonFilled] = useState(false); + const actions = [ + { + title: 'Fill this action button', + onClick: () => setIsActionButtonFilled(!isActionButtonFilled), + iconType: isActionButtonFilled ? 'starFilled' : 'starEmpty', + iconColor: 'primary', + }, + ]; + return ( <> @@ -202,6 +212,22 @@ export const Library: React.FC = () => { + + +

With custom actions

+
+ + + + + + +

With custom actions and a link

+
+ + + +

With field value type highlights

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx index f83ec99acb1ac..7b9dd6b26cbb2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx @@ -13,7 +13,7 @@ import { EuiButton, EuiFormRow, EuiPanel, EuiRange, EuiSpacer } from '@elastic/e import { i18n } from '@kbn/i18n'; import { RelevanceTuningLogic } from '../..'; -import { Boost, BoostType } from '../../types'; +import { Boost, BoostType, FunctionalBoost, ProximityBoost, ValueBoost } from '../../types'; import { FunctionalBoostForm } from './functional_boost_form'; import { ProximityBoostForm } from './proximity_boost_form'; @@ -32,11 +32,11 @@ export const BoostItemContent: React.FC = ({ boost, index, name }) => { const getBoostForm = () => { switch (type) { case BoostType.Value: - return ; + return ; case BoostType.Functional: - return ; + return ; case BoostType.Proximity: - return ; + return ; } }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx index 11a224a71d7f8..feb4328e5adea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx @@ -13,12 +13,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; -import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types'; +import { FunctionalBoost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types'; import { FunctionalBoostForm } from './functional_boost_form'; describe('FunctionalBoostForm', () => { - const boost: Boost = { + const boost: FunctionalBoost = { + value: undefined, factor: 2, type: 'functional' as BoostType, function: 'logarithmic' as FunctionalBoostFunction, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx index d677fe5cbc069..ebd826dcd27ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx @@ -19,7 +19,7 @@ import { FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP, } from '../../constants'; import { - Boost, + FunctionalBoost, BoostFunction, BoostOperation, BoostType, @@ -27,7 +27,7 @@ import { } from '../../types'; interface Props { - boost: Boost; + boost: FunctionalBoost; index: number; name: string; } @@ -39,7 +39,7 @@ const functionOptions = Object.values(FunctionalBoostFunction).map((boostFunctio const operationOptions = Object.values(BoostOperation).map((boostOperation) => ({ value: boostOperation, - text: BOOST_OPERATION_DISPLAY_MAP[boostOperation as BoostOperation], + text: BOOST_OPERATION_DISPLAY_MAP[boostOperation], })); export const FunctionalBoostForm: React.FC = ({ boost, index, name }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx index 6abbcc3d98862..0ed914abb3ab5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx @@ -13,12 +13,14 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiFieldText, EuiSelect } from '@elastic/eui'; -import { Boost, BoostType, ProximityBoostFunction } from '../../types'; +import { ProximityBoost, BoostType, ProximityBoostFunction } from '../../types'; import { ProximityBoostForm } from './proximity_boost_form'; describe('ProximityBoostForm', () => { - const boost: Boost = { + const boost: ProximityBoost = { + value: undefined, + operation: undefined, factor: 2, type: 'proximity' as BoostType, function: 'linear' as ProximityBoostFunction, @@ -46,8 +48,8 @@ describe('ProximityBoostForm', () => { describe('various boost values', () => { const renderWithBoostValues = (boostValues: { - center?: Boost['center']; - function?: Boost['function']; + center?: ProximityBoost['center']; + function?: ProximityBoost['function']; }) => { return shallow( { - const boost: Boost = { + const boost: ValueBoost = { + operation: undefined, + function: undefined, factor: 2, type: 'value' as BoostType, value: ['bar', '', 'baz'], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx index 7fcd07d9a07aa..cd65f2842a5f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx @@ -20,10 +20,10 @@ import { import { i18n } from '@kbn/i18n'; import { RelevanceTuningLogic } from '../..'; -import { Boost } from '../../types'; +import { ValueBoost } from '../../types'; interface Props { - boost: Boost; + boost: ValueBoost; index: number; name: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx index 8a355b97e7b9f..9f6e194f92735 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx @@ -78,6 +78,24 @@ describe('Boosts', () => { expect(select.prop('options').map((o: any) => o.value)).toEqual(['add-boost', 'proximity']); }); + it('will not render functional option if "type" prop is "date"', () => { + const wrapper = shallow( + + ); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual([ + 'add-boost', + 'proximity', + 'value', + ]); + }); + it('will add a boost of the selected type when a selection is made', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx index 4268e21110277..0aa1753938f46 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@ import { i18n } from '@kbn/i18n'; -import { GEOLOCATION, TEXT } from '../../../../shared/constants/field_types'; +import { GEOLOCATION, TEXT, DATE } from '../../../../shared/constants/field_types'; import { SchemaTypes } from '../../../../shared/types'; import { BoostIcon } from '../boost_icon'; @@ -70,6 +70,8 @@ const filterInvalidOptions = (value: BoostType, type: SchemaTypes) => { if (type === TEXT && [BoostType.Proximity, BoostType.Functional].includes(value)) return false; // Value and Functional boost types are not valid for geolocation fields if (type === GEOLOCATION && [BoostType.Functional, BoostType.Value].includes(value)) return false; + // Functional boosts are not valid for date fields + if (type === DATE && value === BoostType.Functional) return false; return true; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 8131a6a3a57c6..181ecad9e9990 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -10,8 +10,11 @@ import { i18n } from '@kbn/i18n'; import { BoostOperation, BoostType, + FunctionalBoost, FunctionalBoostFunction, + ProximityBoost, ProximityBoostFunction, + ValueBoost, } from './types'; export const FIELD_FILTER_CUTOFF = 10; @@ -77,6 +80,38 @@ export const BOOST_TYPE_TO_ICON_MAP = { [BoostType.Proximity]: 'tokenGeo', }; +const EMPTY_VALUE_BOOST: ValueBoost = { + type: BoostType.Value, + factor: 1, + newBoost: true, + function: undefined, + operation: undefined, +}; + +const EMPTY_FUNCTIONAL_BOOST: FunctionalBoost = { + value: undefined, + type: BoostType.Functional, + factor: 1, + newBoost: true, + function: FunctionalBoostFunction.Logarithmic, + operation: BoostOperation.Multiply, +}; + +const EMPTY_PROXIMITY_BOOST: ProximityBoost = { + value: undefined, + type: BoostType.Proximity, + factor: 1, + newBoost: true, + operation: undefined, + function: ProximityBoostFunction.Gaussian, +}; + +export const BOOST_TYPE_TO_EMPTY_BOOST = { + [BoostType.Value]: EMPTY_VALUE_BOOST, + [BoostType.Functional]: EMPTY_FUNCTIONAL_BOOST, + [BoostType.Proximity]: EMPTY_PROXIMITY_BOOST, +}; + export const ADD_DISPLAY = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.addOperationDropDownOptionLabel', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index bcb8ad8a584a5..f0fe98f3f0a87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -719,6 +719,9 @@ describe('RelevanceTuningLogic', () => { factor: 1, newBoost: true, type: BoostType.Functional, + function: 'logarithmic', + operation: 'multiply', + value: undefined, }, ], }, @@ -744,6 +747,9 @@ describe('RelevanceTuningLogic', () => { factor: 1, newBoost: true, type: BoostType.Functional, + function: 'logarithmic', + operation: 'multiply', + value: undefined, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index 0d30296de285f..588b100416d10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -20,15 +20,9 @@ import { RESET_CONFIRMATION_MESSAGE, DELETE_SUCCESS_MESSAGE, DELETE_CONFIRMATION_MESSAGE, + BOOST_TYPE_TO_EMPTY_BOOST, } from './constants'; -import { - BaseBoost, - Boost, - BoostFunction, - BoostOperation, - BoostType, - SearchSettings, -} from './types'; +import { Boost, BoostFunction, BoostOperation, BoostType, SearchSettings } from './types'; import { filterIfTerm, parseBoostCenter, @@ -87,12 +81,12 @@ interface RelevanceTuningActions { updateBoostSelectOption( name: string, boostIndex: number, - optionType: keyof BaseBoost, + optionType: keyof Pick, value: BoostOperation | BoostFunction ): { name: string; boostIndex: number; - optionType: keyof BaseBoost; + optionType: keyof Pick; value: string; }; updateSearchValue(query: string): string; @@ -376,7 +370,7 @@ export const RelevanceTuningLogic = kea< addBoost: ({ name, type }) => { const { searchSettings } = values; const { boosts } = searchSettings; - const emptyBoost = { type, factor: 1, newBoost: true }; + const emptyBoost = BOOST_TYPE_TO_EMPTY_BOOST[type]; let boostArray; if (Array.isArray(boosts[name])) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index 16da5868da681..ec42df218878f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -29,26 +29,38 @@ export enum BoostOperation { Add = 'add', Multiply = 'multiply', } - -export interface BaseBoost { +export interface Boost { + type: BoostType; operation?: BoostOperation; function?: BoostFunction; + newBoost?: boolean; + center?: string | number; + factor: number; + value?: string[]; } // A boost that comes from the server, before we normalize it has a much looser schema -export interface RawBoost extends BaseBoost { - type: BoostType; - newBoost?: boolean; - center?: string | number; +export interface RawBoost extends Omit { value?: string | number | boolean | object | Array; - factor: number; } -// We normalize raw boosts to make them safer and easier to work with -export interface Boost extends RawBoost { +export interface ValueBoost extends Boost { value?: string[]; + operation: undefined; + function: undefined; } +export interface FunctionalBoost extends Boost { + value: undefined; + operation: BoostOperation; + function: FunctionalBoostFunction; +} + +export interface ProximityBoost extends Boost { + value: undefined; + operation: undefined; + function: ProximityBoostFunction; +} export interface SearchField { weight: number; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/index.ts index e46bee43c3933..89909c1e51d3f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/index.ts @@ -6,3 +6,4 @@ */ export { ResultFieldValue } from './result_field_value'; +export { Result } from './result'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 41428999b1e40..86b71229f3785 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -96,6 +96,39 @@ describe('Result', () => { }); }); + describe('actions', () => { + const actions = [ + { + title: 'Hide', + onClick: jest.fn(), + iconType: 'eyeClosed', + iconColor: 'danger', + }, + { + title: 'Bookmark', + onClick: jest.fn(), + iconType: 'starFilled', + iconColor: 'primary', + }, + ]; + + it('will render an action button for each action passed', () => { + const wrapper = shallow(); + expect(wrapper.find('.appSearchResult__actionButton')).toHaveLength(2); + + wrapper.find('.appSearchResult__actionButton').first().simulate('click'); + expect(actions[0].onClick).toHaveBeenCalled(); + + wrapper.find('.appSearchResult__actionButton').last().simulate('click'); + expect(actions[1].onClick).toHaveBeenCalled(); + }); + + it('will render custom actions seamlessly next to the document detail link', () => { + const wrapper = shallow(); + expect(wrapper.find('.appSearchResult__actionButton')).toHaveLength(3); + }); + }); + it('will render field details with type highlights if schemaForTypeHighlights has been provided', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index 7288fdf39f3ff..2812b596e87fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -22,7 +22,7 @@ import { generateEncodedPath } from '../../utils/encode_path_params'; import { ResultField } from './result_field'; import { ResultHeader } from './result_header'; -import { FieldValue, Result as ResultType } from './types'; +import { FieldValue, Result as ResultType, ResultAction } from './types'; interface Props { result: ResultType; @@ -30,6 +30,7 @@ interface Props { showScore?: boolean; shouldLinkToDetailPage?: boolean; schemaForTypeHighlights?: Schema; + actions?: ResultAction[]; } const RESULT_CUTOFF = 5; @@ -40,6 +41,7 @@ export const Result: React.FC = ({ showScore = false, shouldLinkToDetailPage = false, schemaForTypeHighlights, + actions = [], }) => { const [isOpen, setIsOpen] = useState(false); @@ -142,6 +144,18 @@ export const Result: React.FC = ({ )} + {actions.map(({ onClick, title, iconType, iconColor }) => ( + + ))}
); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts index afea65de95db8..96a135b0db36e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts @@ -33,3 +33,10 @@ export type Result = { // You'll need to cast it to FieldValue whenever you use it. [key: string]: object; }; + +export interface ResultAction { + onClick(): void; + title: string; + iconType: string; + iconColor?: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts index 07020105f2096..b605aa5714e91 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/index.ts @@ -6,3 +6,4 @@ */ export { RESULT_SETTINGS_TITLE } from './constants'; +export { ResultSettings } from './result_settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx new file mode 100644 index 0000000000000..8ef7076927307 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { ResultSettings } from './result_settings'; + +describe('RelevanceTuning', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx new file mode 100644 index 0000000000000..6d6d5b2609898 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -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 React from 'react'; + +import { EuiPageHeader, EuiPageContentBody, EuiPageContent } from '@elastic/eui'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + +import { RESULT_SETTINGS_TITLE } from './constants'; + +interface Props { + engineBreadcrumb: string[]; +} + +export const ResultSettings: React.FC = ({ engineBreadcrumb }) => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts new file mode 100644 index 0000000000000..e72f2b90758ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { generateRoleMappingPath } from './utils'; + +describe('generateRoleMappingPath', () => { + it('generates paths with roleId filled', () => { + const roleId = 'role123'; + + expect(generateRoleMappingPath(roleId)).toEqual(`/role_mappings/${roleId}`); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts new file mode 100644 index 0000000000000..109d3de1b86db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ROLE_MAPPING_PATH } from '../../routes'; +import { generateEncodedPath } from '../../utils/encode_path_params'; + +export const generateRoleMappingPath = (roleId: string) => + generateEncodedPath(ROLE_MAPPING_PATH, { roleId }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 6827f4f5e827d..4da71ec9a135b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -156,7 +156,7 @@ describe('AppSearchNav', () => { const wrapper = shallow(); expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( - 'http://localhost:3002/as#/role-mappings' + 'http://localhost:3002/as/role_mappings' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 907a27c8660d2..9ab67601969d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -14,7 +14,10 @@ export const SETUP_GUIDE_PATH = '/setup_guide'; export const LIBRARY_PATH = '/library'; export const SETTINGS_PATH = '/settings/account'; export const CREDENTIALS_PATH = '/credentials'; -export const ROLE_MAPPINGS_PATH = '#/role-mappings'; // This page seems to 404 if the # isn't included + +export const ROLE_MAPPINGS_PATH = '/role_mappings'; +export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`; +export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`; export const ENGINES_PATH = '/engines'; export const ENGINE_CREATION_PATH = '/engine_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts index 0918d2025f732..ecbf885ac3b5c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts @@ -7,7 +7,7 @@ import { roleHasScopedEngines } from './'; -describe('roleHasScopedEngines()', () => { +describe('roleHasScopedEngines', () => { it('returns false for owner and admin roles', () => { expect(roleHasScopedEngines('owner')).toEqual(false); expect(roleHasScopedEngines('admin')).toEqual(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts index 6e9c867b15679..1576fa178cfa9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { AttributeName } from '../../types'; + export const asRoleMapping = { id: null, - attributeName: 'role', + attributeName: 'role' as AttributeName, attributeValue: ['superuser'], authProvider: ['*'], roleType: 'owner', @@ -23,7 +25,7 @@ export const asRoleMapping = { export const wsRoleMapping = { id: '602d4ba85foobarbaz123', - attributeName: 'username', + attributeName: 'username' as AttributeName, attributeValue: 'user', authProvider: ['*', 'other_auth'], roleType: 'admin', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index bc31732527b0e..b5d1ebb899ba1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -11,7 +11,9 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiComboBox, EuiFieldText } from '@elastic/eui'; -import { AttributeSelector, AttributeName } from './attribute_selector'; +import { AttributeName } from '../types'; + +import { AttributeSelector } from './attribute_selector'; import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; const handleAttributeSelectorChange = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index d19107b534fb7..48d1447e9bd0f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -20,6 +20,8 @@ import { EuiTitle, } from '@elastic/eui'; +import { AttributeName, AttributeExamples } from '../types'; + import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, @@ -31,8 +33,6 @@ import { ATTRIBUTE_VALUE_LABEL, } from './constants'; -export type AttributeName = keyof AttributeExamples | 'role'; - interface Props { attributeName: AttributeName; attributeValue?: string; @@ -47,12 +47,6 @@ interface Props { handleAuthProviderChange?(value: string[]): void; } -interface AttributeExamples { - username: string; - email: string; - metadata: string; -} - interface ParentOption extends EuiComboBoxOptionOption { label: string; options: ChildOption[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 1fbbc172dcf69..9eae2ce06efa2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -107,3 +107,25 @@ export const MANAGE_ROLE_MAPPING_BUTTON = i18n.translate( defaultMessage: 'Manage', } ); + +export const ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsTitle', + { + defaultMessage: 'Users & roles', + } +); + +export const EMPTY_ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.emptyRoleMappingsTitle', + { + defaultMessage: 'No role mappings yet', + } +); + +export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsDescription', + { + defaultMessage: + 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 16a9e3b54aea5..e026e2f592e75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -51,9 +51,17 @@ export interface RoleRules { metadata?: string; } +export interface AttributeExamples { + username: string; + email: string; + metadata: string; +} + +export type AttributeName = keyof AttributeExamples | 'role'; + export interface RoleMapping { id: string; - attributeName: string; + attributeName: AttributeName; attributeValue: string; authProvider: string[]; roleType: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg index 23eff13915401..7aac36a6fe3c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg deleted file mode 100644 index d241989f1aff1..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg index f8f6415dea22b..cc07fbbc50877 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg deleted file mode 100644 index 40b65df3a1ce3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg index d16f293fde6dc..01e5a7735de12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg index c4b4176560d5b..aa9c3e5b45146 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg index ae068feb7133d..31fe60c6a63f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg deleted file mode 100644 index 22630f533dcbf..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg index c684cecb71235..f3fe82cd3cd98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts index 347dee11670c9..e6a994d05f3ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts @@ -7,24 +7,18 @@ import box from './box.svg'; import confluence from './confluence.svg'; -import crawler from './crawler.svg'; import custom from './custom.svg'; -import drive from './drive.svg'; import dropbox from './dropbox.svg'; import github from './github.svg'; import gmail from './gmail.svg'; -import google from './google.svg'; import googleDrive from './google_drive.svg'; import jira from './jira.svg'; import jiraServer from './jira_server.svg'; import loadingSmall from './loading_small.svg'; -import office365 from './office365.svg'; -import oneDrive from './one_drive.svg'; -import outlook from './outlook.svg'; -import people from './people.svg'; +import oneDrive from './onedrive.svg'; import salesforce from './salesforce.svg'; -import serviceNow from './service_now.svg'; -import sharePoint from './share_point.svg'; +import serviceNow from './servicenow.svg'; +import sharePoint from './sharepoint.svg'; import slack from './slack.svg'; import zendesk from './zendesk.svg'; @@ -33,23 +27,17 @@ export const images = { confluence, confluenceCloud: confluence, confluenceServer: confluence, - crawler, custom, - drive, dropbox, github, githubEnterpriseServer: github, gmail, googleDrive, - google, jira, jiraServer, jiraCloud: jira, loadingSmall, - office365, oneDrive, - outlook, - people, salesforce, salesforceSandbox: salesforce, serviceNow, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg index 224bb822a581c..c12e55798d889 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg index 71750fb6e25a0..4dfd0fd910079 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg deleted file mode 100644 index fdce5d02da3cd..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg deleted file mode 100644 index 1856e5e3ce1af..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/onedrive.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/onedrive.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg deleted file mode 100644 index 2680bc99cc367..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg deleted file mode 100644 index 4500c494c23b7..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg index 510c438a28195..ef6d552949424 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg deleted file mode 100644 index 2d0c09db4e1c3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/servicenow.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/servicenow.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg index f8d2ea1e634f6..39c40a65dfa70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg deleted file mode 100644 index 8724be9da88cf..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg index 14dbd0289da84..8f6fc0c987eaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg index f7bc1fda0c9ac..8afd143dd9a7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg deleted file mode 100644 index 827f8cf0a55ec..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg deleted file mode 100644 index 7aac36a6fe3c5..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg deleted file mode 100644 index cc07fbbc50877..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg deleted file mode 100644 index 01e5a7735de12..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg deleted file mode 100644 index aa9c3e5b45146..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg deleted file mode 100644 index 31fe60c6a63f9..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg deleted file mode 100644 index f3fe82cd3cd98..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts deleted file mode 100644 index d9a0975abef7c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts +++ /dev/null @@ -1,45 +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 box from './box.svg'; -import confluence from './confluence.svg'; -import custom from './custom.svg'; -import dropbox from './dropbox.svg'; -import github from './github.svg'; -import gmail from './gmail.svg'; -import googleDrive from './google_drive.svg'; -import jira from './jira.svg'; -import jiraServer from './jira_server.svg'; -import oneDrive from './onedrive.svg'; -import salesforce from './salesforce.svg'; -import serviceNow from './servicenow.svg'; -import sharePoint from './sharepoint.svg'; -import slack from './slack.svg'; -import zendesk from './zendesk.svg'; - -export const imagesFull = { - box, - confluence, - confluenceCloud: confluence, - confluenceServer: confluence, - custom, - dropbox, - github, - githubEnterpriseServer: github, - gmail, - googleDrive, - jira, - jiraServer, - jiraCloud: jira, - oneDrive, - salesforce, - salesforceSandbox: salesforce, - serviceNow, - sharePoint, - slack, - zendesk, -} as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg deleted file mode 100644 index c12e55798d889..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg deleted file mode 100644 index 4dfd0fd910079..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg deleted file mode 100644 index ef6d552949424..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg deleted file mode 100644 index 8f6fc0c987eaa..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg deleted file mode 100644 index 8afd143dd9a7c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx index 3bea6f224dc2b..ee079970a5ebb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -26,10 +26,4 @@ describe('SourceIcon', () => { expect(wrapper.find('.wrapped-icon')).toHaveLength(1); }); - - it('renders a full bleed icon', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiIcon).prop('type')).toEqual('test-file-stub'); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx index 03106dd7d8b8f..1d1462542a3f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -14,14 +14,12 @@ import { EuiIcon, IconSize } from '@elastic/eui'; import './source_icon.scss'; import { images } from '../assets/source_icons'; -import { imagesFull } from '../assets/sources_full_bleed'; interface SourceIconProps { serviceType: string; name: string; className?: string; wrapped?: boolean; - fullBleed?: boolean; size?: IconSize; } @@ -30,16 +28,10 @@ export const SourceIcon: React.FC = ({ serviceType, className, wrapped, - fullBleed = false, size = 'xxl', }) => { const icon = ( - + ); return wrapped ? (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx index bf472240d3c89..2ecb3c98565b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx @@ -34,12 +34,7 @@ export const AddSourceHeader: React.FC = ({ responsive={false} > - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 765836191ff00..25c78afbe4e05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -41,7 +41,6 @@ export const SourceInfoCard: React.FC = ({ className="content-source-meta__icon" serviceType={sourceType} name={sourceType} - fullBleed size="l" /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts index a4b4e4a1bfd29..a44144666d139 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -59,13 +59,6 @@ export const GROUP_ASSIGNMENT_ALL_GROUPS_LABEL = i18n.translate( } ); -export const EMPTY_ROLE_MAPPINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsTitle', - { - defaultMessage: 'No role mappings yet', - } -); - export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody', { @@ -80,18 +73,3 @@ export const ROLE_MAPPINGS_TABLE_HEADER = i18n.translate( defaultMessage: 'Group Access', } ); - -export const ROLE_MAPPINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTitle', - { - defaultMessage: 'Users & roles', - } -); - -export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsDescription', - { - defaultMessage: - 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index e47b2646459df..842c59e683f06 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -14,16 +14,15 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { getRoleMappingPath, ROLE_MAPPING_NEW_PATH } from '../../routes'; - import { EMPTY_ROLE_MAPPINGS_TITLE, - EMPTY_ROLE_MAPPINGS_BODY, - ROLE_MAPPINGS_TABLE_HEADER, ROLE_MAPPINGS_TITLE, ROLE_MAPPINGS_DESCRIPTION, -} from './constants'; +} from '../../../shared/role_mapping/constants'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { getRoleMappingPath, ROLE_MAPPING_NEW_PATH } from '../../routes'; + +import { EMPTY_ROLE_MAPPINGS_BODY, ROLE_MAPPINGS_TABLE_HEADER } from './constants'; import { RoleMappingsLogic } from './role_mappings_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 6fc3867d7ab1e..b43bda3bb228e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -10,8 +10,8 @@ import { kea, MakeLogicType } from 'kea'; import { clearFlashMessages, flashAPIErrors } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; -import { AttributeName } from '../../../shared/role_mapping/attribute_selector'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { AttributeName } from '../../../shared/types'; import { ROLE_MAPPINGS_PATH } from '../../routes'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 90b86138a4a6d..74f13a05aa7e6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -12,6 +12,7 @@ import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; import { registerEnginesRoutes } from './engines'; +import { registerRoleMappingsRoutes } from './role_mappings'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSettingsRoutes } from './settings'; @@ -24,4 +25,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerDocumentRoutes(dependencies); registerCurationsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); + registerRoleMappingsRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts new file mode 100644 index 0000000000000..53368035af225 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts @@ -0,0 +1,215 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { + registerRoleMappingsRoute, + registerRoleMappingRoute, + registerNewRoleMappingRoute, + registerResetRoleMappingRoute, +} from './role_mappings'; + +const roleMappingBaseSchema = { + rules: { username: 'user' }, + roleType: 'owner', + engines: ['e1', 'e2'], + accessAllEngines: false, + authProvider: ['*'], +}; + +describe('role mappings routes', () => { + describe('GET /api/app_search/role_mappings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/role_mappings', + }); + + registerRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings', + }); + }); + }); + + describe('POST /api/app_search/role_mappings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/role_mappings', + }); + + registerRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: roleMappingBaseSchema }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('GET /api/app_search/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/role_mappings/{id}', + }); + + registerRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/:id', + }); + }); + }); + + describe('PUT /api/app_search/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/role_mappings/{id}', + }); + + registerRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/:id', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + ...roleMappingBaseSchema, + id: '123', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('DELETE /api/app_search/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/role_mappings/{id}', + }); + + registerRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/:id', + }); + }); + }); + + describe('GET /api/app_search/role_mappings/new', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/role_mappings/new', + }); + + registerNewRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/new', + }); + }); + }); + + describe('GET /api/app_search/role_mappings/reset', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/role_mappings/reset', + }); + + registerResetRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/reset', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts new file mode 100644 index 0000000000000..4b77c8614a52c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +const roleMappingBaseSchema = { + rules: schema.recordOf(schema.string(), schema.string()), + roleType: schema.string(), + engines: schema.arrayOf(schema.string()), + accessAllEngines: schema.boolean(), + authProvider: schema.arrayOf(schema.string()), +}; + +export function registerRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/role_mappings', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings', + }) + ); + + router.post( + { + path: '/api/app_search/role_mappings', + validate: { + body: schema.object(roleMappingBaseSchema), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings', + }) + ); +} + +export function registerRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/role_mappings/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/:id', + }) + ); + + router.put( + { + path: '/api/app_search/role_mappings/{id}', + validate: { + body: schema.object({ + ...roleMappingBaseSchema, + id: schema.string(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/:id', + }) + ); + + router.delete( + { + path: '/api/app_search/role_mappings/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/:id', + }) + ); +} + +export function registerNewRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/role_mappings/new', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/new', + }) + ); +} + +export function registerResetRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/role_mappings/reset', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/reset', + }) + ); +} + +export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerRoleMappingsRoute(dependencies); + registerRoleMappingRoute(dependencies); + registerNewRoleMappingRoute(dependencies); + registerResetRoleMappingRoute(dependencies); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 0910642581691..b56c5880dba43 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -857,9 +857,10 @@ export function registerOauthConnectorParamsRoute({ validate: { query: schema.object({ kibana_host: schema.string(), - code: schema.string(), + code: schema.maybe(schema.string()), session_state: schema.maybe(schema.string()), state: schema.string(), + oauth_token: schema.maybe(schema.string()), oauth_verifier: schema.maybe(schema.string()), }), }, 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 b692d7fe69cd4..2b5319cafb1d4 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 @@ -208,8 +208,8 @@ export const setup = async (arg?: { }; }; - const setFreeze = createFormToggleAction('freezeSwitch'); - const freezeExists = () => exists('freezeSwitch'); + const createSetFreeze = (phase: Phases) => createFormToggleAction(`${phase}-freezeSwitch`); + const createFreezeExists = (phase: Phases) => () => exists(`${phase}-freezeSwitch`); const createReadonlyActions = (phase: Phases) => { const toggleSelector = `${phase}-readonlySwitch`; @@ -275,21 +275,21 @@ export const setup = async (arg?: { const dataTierSelector = `${controlsSelector}.dataTierSelect`; const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + const openNodeAttributesSection = async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }; + return { hasDataTierAllocationControls: () => exists(controlsSelector), - openNodeAttributesSection: async () => { - await act(async () => { - find(dataTierSelector).simulate('click'); - }); - component.update(); - }, + openNodeAttributesSection, hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), setDataAllocation: async (value: DataTierAllocationType) => { - act(() => { - find(dataTierSelector).simulate('click'); - }); - component.update(); + await openNodeAttributesSection(); + await act(async () => { switch (value) { case 'node_roles': @@ -359,6 +359,7 @@ export const setup = async (arg?: { hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), hasColdPhase: () => exists('ilmTimelineColdPhase'), + hasFrozenPhase: () => exists('ilmTimelineFrozenPhase'), hasDeletePhase: () => exists('ilmTimelineDeletePhase'), }, hot: { @@ -390,13 +391,19 @@ export const setup = async (arg?: { enable: enable('cold'), ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), - setFreeze, - freezeExists, + setFreeze: createSetFreeze('cold'), + freezeExists: createFreezeExists('cold'), hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), ...createNodeAllocationActions('cold'), }, + frozen: { + enable: enable('frozen'), + ...createMinAgeActions('frozen'), + hasErrorIndicator: () => exists('phaseErrorIndicator-frozen'), + ...createSearchableSnapshotActions('frozen'), + }, delete: { isShown: () => exists('delete-phaseContent'), ...createToggleDeletePhaseActions(), 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 new file mode 100644 index 0000000000000..3103a4198fc3b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; + +import { licensingMock } from '../../../../../licensing/public/mocks'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getDefaultHotPhasePolicy } from '../constants'; + +describe(' frozen phase', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('shows timing only when enabled', async () => { + const { actions, exists } = testBed; + + expect(exists('frozen-phase')).toBe(true); + expect(actions.frozen.hasMinAgeInput()).toBeFalsy(); + await actions.frozen.enable(true); + expect(actions.frozen.hasMinAgeInput()).toBeTruthy(); + }); + + describe('on non-enterprise license', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + + test('should not be available', async () => { + const { exists } = testBed; + expect(exists('frozen-phase')).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts index 13e55a1f39e2c..832963827663d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts @@ -216,6 +216,7 @@ describe(' node allocation', () => { test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { const { actions, component } = testBed; + await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -250,23 +251,37 @@ describe(' node allocation', () => { expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); }); - test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, + [ + { + nodesByRoles: { data_hot: ['test'] }, + previousActiveRole: 'hot', + }, + { nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); + previousActiveRole: 'warm', + }, + ].forEach(({ nodesByRoles, previousActiveRole }) => { + test(`shows default allocation notice when ${previousActiveRole} tiers exists, but not cold tier`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles, + isUsingDeprecatedDataRoleConfig: false, + }); - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; + await act(async () => { + testBed = await setup(); + }); + const { actions, component, find } = testBed; - component.update(); - await actions.cold.enable(true); + component.update(); + await actions.cold.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + expect(find('defaultAllocationNotice').text()).toContain( + `This policy will move data in the cold phase to ${previousActiveRole} tier nodes` + ); + }); }); test(`doesn't show default allocation notice when node with "data" role exists`, async () => { @@ -366,7 +381,7 @@ describe(' node allocation', () => { expect(find('cloudDataTierCallout').exists()).toBeFalsy(); }); - test('shows cloud notice when cold tier nodes do not exist', async () => { + test(`shows cloud notice when cold tier nodes do not exist`, async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: {}, nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, @@ -375,13 +390,17 @@ describe(' node allocation', () => { await act(async () => { testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); }); - const { actions, component, exists } = testBed; + const { actions, component, exists, find } = testBed; component.update(); await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(exists('cloudMissingColdTierCallout')).toBeTruthy(); + expect(exists('cloudMissingTierCallout')).toBeTruthy(); + expect(find('cloudMissingTierCallout').text()).toContain( + `Edit your Elastic Cloud deployment to set up a cold tier` + ); + // Assert that other notices are not showing expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); @@ -480,6 +499,7 @@ describe(' node allocation', () => { const { find } = testBed; expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); }); + test('detecting use of the "off" allocation type', () => { const { find } = testBed; expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); 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 e2b67efbf588d..506ac4cece032 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 @@ -45,6 +45,7 @@ describe(' timeline', () => { const { actions } = testBed; expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); }); + test('hides forcemerge when rollover is disabled', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); @@ -56,22 +57,26 @@ describe(' timeline', () => { const { actions } = testBed; expect(actions.hot.shrinkExists()).toBeTruthy(); }); + test('hides shrink input when rollover is disabled', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); expect(actions.hot.shrinkExists()).toBeFalsy(); }); + test('shows readonly input when rollover enabled', async () => { const { actions } = testBed; expect(actions.hot.readonlyExists()).toBeTruthy(); }); + test('hides readonly input when rollover is disabled', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); expect(actions.hot.readonlyExists()).toBeFalsy(); }); + test('hides and disables searchable snapshot field', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); @@ -86,12 +91,15 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); + await actions.frozen.enable(true); await actions.delete.enablePhase(); expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy(); + expect(actions.frozen.hasRolloverTipOnMinAge()).toBeTruthy(); expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy(); }); + test('hiding rollover tip on minimum age', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); @@ -99,10 +107,12 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); + await actions.frozen.enable(true); await actions.delete.enablePhase(); expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy(); + expect(actions.frozen.hasRolloverTipOnMinAge()).toBeFalsy(); expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy(); }); }); 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 ed678a6b217ae..44e03564cb89a 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 @@ -94,6 +94,7 @@ describe(' searchable snapshots', () => { ).toBe(true); }); }); + describe('existing policy', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -124,6 +125,7 @@ describe(' searchable snapshots', () => { }); }); }); + describe('on non-enterprise license', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -145,11 +147,13 @@ describe(' searchable snapshots', () => { const { component } = testBed; component.update(); }); + test('disable setting searchable snapshots', async () => { const { actions } = testBed; - expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.frozen.searchableSnapshotsExists()).toBeFalsy(); await actions.cold.enable(true); 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 6fe968edb9b05..56f5815633a1d 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 @@ -21,5 +21,6 @@ export type TestSubjects = | 'policyTablePolicyNameLink' | 'policyTitle' | 'createPolicyButton' - | 'freezeSwitch' + | 'cold-freezeSwitch' + | 'frozen-freezeSwitch' | string; diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts b/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts index dcf8ce51a65ad..b00bb617b0be8 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts @@ -13,7 +13,15 @@ const WARM_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_warm', 'data_hot']; const COLD_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_cold', 'data_warm', 'data_hot']; +const FROZEN_PHASE_NODE_PREFERENCE: DataTierRole[] = [ + 'data_frozen', + 'data_cold', + 'data_warm', + 'data_hot', +]; + export const phaseToNodePreferenceMap: Record = Object.freeze({ warm: WARM_PHASE_NODE_PREFERENCE, cold: COLD_PHASE_NODE_PREFERENCE, + frozen: FROZEN_PHASE_NODE_PREFERENCE, }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/index.ts b/x-pack/plugins/index_lifecycle_management/common/types/index.ts index 3cd01f975d3b3..bc7e881a8c230 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/index.ts @@ -12,7 +12,7 @@ export * from './policies'; /** * These roles reflect how nodes are stratified into different data tiers. */ -export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold'; +export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold' | 'data_frozen'; /** * The "data_content" role can store all data the ES stack uses for feature 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 9f65286dc9b30..d3fec300d2d5f 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -7,7 +7,7 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; -export type PhaseWithAllocation = 'warm' | 'cold'; +export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen'; export interface SerializedPolicy { name: string; @@ -18,6 +18,7 @@ export interface Phases { hot?: SerializedHotPhase; warm?: SerializedWarmPhase; cold?: SerializedColdPhase; + frozen?: SerializedFrozenPhase; delete?: SerializedDeletePhase; } @@ -51,6 +52,8 @@ export interface SerializedActionWithAllocation { migrate?: MigrateAction; } +export type SearchableSnapshotStorage = 'full_copy' | 'shared_cache'; + export interface SearchableSnapshotAction { snapshot_repository: string; /** @@ -58,6 +61,12 @@ export interface SearchableSnapshotAction { * not suit the vast majority of cases. */ force_merge_index?: boolean; + /** + * This configuration lets the user create full or partial searchable snapshots. + * Full searchable snapshots store primary data locally and store replica data in the snapshot. + * Partial searchable snapshots store no data locally. + */ + storage?: SearchableSnapshotStorage; } export interface RolloverAction { @@ -111,6 +120,21 @@ export interface SerializedColdPhase extends SerializedPhase { }; } +export interface SerializedFrozenPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + migrate?: MigrateAction; + /** + * Only available on enterprise license + */ + searchable_snapshot?: SearchableSnapshotAction; + }; +} + export interface SerializedDeletePhase extends SerializedPhase { actions: { wait_for_snapshot?: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 39c52391927a0..6e3134f9f2428 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -11,6 +11,7 @@ export const defaultIndexPriority = { hot: '100', warm: '50', cold: '0', + frozen: '0', }; export const defaultRolloverAction: RolloverAction = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss index 7c6a5aefdde6e..5bd6790dda572 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss @@ -23,6 +23,9 @@ &__inner--cold { fill: $euiColorVis1_behindText; } + &__inner--frozen { + fill: $euiColorVis4_behindText; + } &__inner--delete { fill: $euiColorDarkShade; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 1dbc30674eaa5..bc22516e6c996 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -6,20 +6,15 @@ */ import React, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiTextColor } from '@elastic/eui'; - import { useConfigurationIssues } from '../../../form'; - -import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; - import { DataTierAllocationField, SearchableSnapshotField, IndexPriorityField, ReplicasField, + FreezeField, } from '../shared_fields'; import { Phase } from '../phase'; @@ -41,35 +36,7 @@ export const ColdPhase: FunctionComponent = () => { {/* Freeze section */} - {!isUsingSearchableSnapshotInHotPhase && ( - - - - } - description={ - - {' '} - - - } - fullWidth - titleSize="xs" - switchProps={{ - 'data-test-subj': 'freezeSwitch', - path: '_meta.cold.freezeEnabled', - }} - > -
- - )} + {!isUsingSearchableSnapshotInHotPhase && } {/* Data tier allocation section */} { ); return ( - } - className="ilmDeletePhase ilmPhase" - timelineIcon={} - > - - {i18nTexts.editPolicy.descriptions.delete} - + <> - - + } + className="ilmDeletePhase ilmPhase" + timelineIcon={} + > + + {i18nTexts.editPolicy.descriptions.delete} + + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx new file mode 100644 index 0000000000000..41cdb2a5f10fa --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx @@ -0,0 +1,20 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { SearchableSnapshotField } from '../shared_fields'; +import { Phase } from '../phase'; + +export const FrozenPhase: FunctionComponent = () => { + return ( + } + /> + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts new file mode 100644 index 0000000000000..e0f5ac9189adc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { FrozenPhase } from './frozen_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts index 62807d9499243..b248c15817548 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts @@ -11,6 +11,8 @@ export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; +export { FrozenPhase } from './frozen_phase'; + export { DeletePhase } from './delete_phase'; export { Phase } from './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 3a057f6204e24..040baf3625eb8 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 @@ -23,16 +23,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { PhasesExceptDelete } from '../../../../../../../common/types'; import { ToggleField, useFormData } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; - import { FormInternal } from '../../../types'; - import { UseField } from '../../../form'; - -import { PhaseErrorIndicator } from './phase_error_indicator'; - import { MinAgeField } from '../shared_fields'; import { PhaseIcon } from '../../phase_icon'; import { PhaseFooter } from '../../phase_footer'; +import { PhaseErrorIndicator } from './phase_error_indicator'; + import './phase.scss'; interface Props { @@ -99,6 +96,7 @@ export const Phase: FunctionComponent = ({ children, topLevelSettings, ph actions={minAge} timelineIcon={} className={`ilmPhase ${enabled ? 'ilmPhase--enabled' : ''}`} + data-test-subj={`${phase}-phase`} > {i18nTexts.editPolicy.descriptions[phase]} @@ -115,20 +113,28 @@ export const Phase: FunctionComponent = ({ children, topLevelSettings, ph )} - - } - buttonClassName="ilmSettingsButton" - extraAction={} - > - - {children} - + {children ? ( + + } + buttonClassName="ilmSettingsButton" + extraAction={} + > + + {children} + + ) : ( + + + + + + )} )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx index b7a437d85add0..254063ac1a9ea 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx @@ -24,6 +24,17 @@ import './data_tier_allocation.scss'; type SelectOptions = EuiSuperSelectOption; +const customTexts = { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), +}; + const i18nTexts = { allocationOptions: { warm: { @@ -47,16 +58,7 @@ const i18nTexts = { { defaultMessage: 'Do not move data in the warm phase.' } ), }, - custom: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', - { defaultMessage: 'Custom' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } - ), - }, + custom: customTexts, }, cold: { default: { @@ -79,16 +81,30 @@ const i18nTexts = { { defaultMessage: 'Do not move data in the cold phase.' } ), }, - custom: { + custom: customTexts, + }, + frozen: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.input', + { defaultMessage: 'Use frozen nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the frozen tier.' } + ), + }, + none: { inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', - { defaultMessage: 'Custom' } + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.input', + { defaultMessage: 'Off' } ), helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.helpText', + { defaultMessage: 'Do not move data in the frozen phase.' } ), }, + custom: customTexts, }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx index e43b750849774..b09d09d254085 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx @@ -21,6 +21,9 @@ const i18nTextsNodeRoleToDataTier: Record = { data_cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel', { defaultMessage: 'cold', }), + data_frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierFrozenLabel', { + defaultMessage: 'frozen', + }), }; const i18nTexts = { @@ -49,6 +52,21 @@ const i18nTexts = { values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, }), }, + frozen: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen.title', + { defaultMessage: 'No nodes assigned to the frozen tier' } + ), + body: (nodeRole: DataTierRole) => + i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen', + { + defaultMessage: + 'This policy will move data in the frozen phase to {tier} tier nodes instead.', + values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, + } + ), + }, }, warning: { warm: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx index a194f3c07f900..649eb9f2fcb7f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx @@ -39,6 +39,19 @@ const i18nTexts = { } ), }, + frozen: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the frozen tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the frozen, cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index 938e0a850f933..dacec1df52e2e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -17,7 +17,7 @@ export { DefaultAllocationWarning } from './default_allocation_warning'; export { NoNodeAttributesWarning } from './no_node_attributes_warning'; -export { MissingColdTierCallout } from './missing_cold_tier_callout'; +export { MissingCloudTierCallout } from './missing_cloud_tier_callout'; export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx similarity index 70% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx index 21b8850e0b088..09d3135cde469 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx @@ -9,20 +9,23 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', { - defaultMessage: 'Create a cold tier', +const geti18nTexts = (tier: 'cold' | 'frozen') => ({ + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.title', { + defaultMessage: 'Create a {tier} tier', + values: { tier }, }), - body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a {tier} tier.', + values: { tier }, }), linkText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription', + 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.linkToCloudDeploymentDescription', { defaultMessage: 'View cloud deployment' } ), -}; +}); interface Props { + phase: 'cold' | 'frozen'; linkToCloudDeployment?: string; } @@ -31,9 +34,14 @@ interface Props { * This may need to be change when we have autoscaling enabled on a cluster because nodes may not * yet exist, but will automatically be provisioned. */ -export const MissingColdTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { +export const MissingCloudTierCallout: FunctionComponent = ({ + phase, + linkToCloudDeployment, +}) => { + const i18nTexts = geti18nTexts(phase); + return ( - + {i18nTexts.body}{' '} {Boolean(linkToCloudDeployment) && ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx index c4ca0c5e495e1..e6cadf7049962 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx @@ -33,6 +33,15 @@ const i18nTexts = { } ), }, + frozen: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', + } + ), + }, }; export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index 7a660e0379a8d..ef0e82063ce20 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -25,7 +25,7 @@ import { DefaultAllocationNotice, DefaultAllocationWarning, NoNodeAttributesWarning, - MissingColdTierCallout, + MissingCloudTierCallout, CloudDataTierCallout, LoadingError, } from './components'; @@ -71,17 +71,21 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr switch (allocationType) { case 'node_roles': /** - * We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role. + * We'll drive Cloud users to add a cold or frozen tier to their deployment if there are no nodes with that role. */ - if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) { - const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; + if ( + isCloudEnabled && + !isUsingDeprecatedDataRoleConfig && + (phase === 'cold' || phase === 'frozen') + ) { + const hasNoNodesWithNodeRole = !nodesByRoles[`data_${phase}` as const]?.length; if (hasDataNodeRoles && hasNoNodesWithNodeRole) { // Tell cloud users they can deploy nodes on cloud. return ( <> - + ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx new file mode 100644 index 0000000000000..8db1829f03764 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTextColor } from '@elastic/eui'; + +import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; + +interface Props { + phase: 'cold' | 'frozen'; +} + +export const FreezeField: FunctionComponent = ({ phase }) => { + return ( + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + switchProps={{ + 'data-test-subj': `${phase}-freezeSwitch`, + path: `_meta.${phase}.freezeEnabled`, + }} + > +
+ + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 220f0bd8e941a..91faf5c66df81 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -22,3 +22,5 @@ export { ReadonlyField } from './readonly_field'; export { ReplicasField } from './replicas_field'; export { IndexPriorityField } from './index_priority_field'; + +export { FreezeField } from './freeze_field'; 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 507a99c4754b8..47d2aa6ba92df 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 @@ -18,7 +18,7 @@ import { UseField } from '../../../form'; import { LearnMoreLink, DescribedFormRow } from '../..'; interface Props { - phase: 'hot' | 'warm' | 'cold'; + phase: 'hot' | 'warm' | 'cold' | 'frozen'; } export const IndexPriorityField: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx index 4f005eb4fd201..ea05e401822ce 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx @@ -16,7 +16,7 @@ import { UseField } from '../../../form'; import { DescribedFormRow } from '../../described_form_row'; interface Props { - phase: 'warm' | 'cold'; + phase: 'warm' | 'cold' | 'frozen'; } export const ReplicasField: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 3fc7064575555..816e1aaec31d7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -17,28 +17,18 @@ import { EuiLink, } from '@elastic/eui'; -import { - ComboBoxField, - useKibana, - fieldValidators, - useFormData, -} from '../../../../../../../shared_imports'; +import { ComboBoxField, useKibana, useFormData } from '../../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../../edit_policy_context'; -import { useConfigurationIssues, UseField } from '../../../../form'; - -import { i18nTexts } from '../../../../i18n_texts'; - +import { useConfigurationIssues, UseField, searchableSnapshotFields } from '../../../../form'; import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../'; - import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider'; import './_searchable_snapshot_field.scss'; -const { emptyField } = fieldValidators; - export interface Props { - phase: 'hot' | 'cold'; + phase: 'hot' | 'cold' | 'frozen'; + canBeDisabled?: boolean; } /** @@ -47,29 +37,62 @@ export interface Props { */ const CLOUD_DEFAULT_REPO = 'found-snapshots'; -export const SearchableSnapshotField: FunctionComponent = ({ phase }) => { +const geti18nTexts = (phase: Props['phase']) => ({ + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { + defaultMessage: 'Searchable snapshot', + }), + description: + phase === 'frozen' ? ( + + ), + }} + /> + ) : ( + , + }} + /> + ), +}); + +export const SearchableSnapshotField: FunctionComponent = ({ + phase, + canBeDisabled = true, +}) => { const { services: { cloud }, } = useKibana(); const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext(); const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); - const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; + const searchableSnapshotRepoPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; - const [formData] = useFormData({ watch: searchableSnapshotPath }); - const searchableSnapshotRepo = get(formData, searchableSnapshotPath); + const [formData] = useFormData({ watch: searchableSnapshotRepoPath }); + const searchableSnapshotRepo = get(formData, searchableSnapshotRepoPath); const isColdPhase = phase === 'cold'; + const isFrozenPhase = phase === 'frozen'; + const isColdOrFrozenPhase = isColdPhase || isFrozenPhase; const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => Boolean( - // New policy on cloud should have searchable snapshot on in cold phase - (isColdPhase && isNewPolicy && cloud?.isCloudEnabled) || + // New policy on cloud should have searchable snapshot on in cold and frozen phase + (isColdOrFrozenPhase && isNewPolicy && cloud?.isCloudEnabled) || policy.phases[phase]?.actions?.searchable_snapshot?.snapshot_repository ) ); + const i18nTexts = geti18nTexts(phase); + useEffect(() => { if (isDisabledDueToLicense) { setIsFieldToggleChecked(false); @@ -180,17 +203,10 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
config={{ - label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, + ...searchableSnapshotFields.snapshot_repository, defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, - validations: [ - { - validator: emptyField( - i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired - ), - }, - ], }} - path={searchableSnapshotPath} + path={searchableSnapshotRepoPath} > {(field) => { const singleSelectionArray: [selectedSnapshot?: string] = field.value @@ -289,34 +305,24 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => return ( - {i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { - defaultMessage: 'Searchable snapshot', - })} - + switchProps={ + canBeDisabled + ? { + checked: isFieldToggleChecked, + disabled: isDisabledDueToLicense, + onChange: setIsFieldToggleChecked, + 'data-test-subj': 'searchableSnapshotToggle', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel', + { defaultMessage: 'Create searchable snapshot' } + ), + } + : undefined } + title={

{i18nTexts.title}

} description={ <> - - , - }} - /> - + {i18nTexts.description} } fieldNotices={renderInfoCallout()} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index 2f5d00082cc8a..93547fdebffe5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -42,6 +42,7 @@ const prettifyFormJson = (policy: SerializedPolicy): SerializedPolicy => ({ hot: policy.phases.hot, warm: policy.phases.warm, cold: policy.phases.cold, + frozen: policy.phases.frozen, delete: policy.phases.delete, }, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx index c2aa011f582c7..88d9d2de03d89 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx @@ -26,6 +26,7 @@ export const Timeline: FunctionComponent = () => { hotPhaseMinAge={timings.hot.min_age} warmPhaseMinAge={timings.warm?.min_age} coldPhaseMinAge={timings.cold?.min_age} + frozenPhaseMinAge={timings.frozen?.min_age} deletePhaseMinAge={timings.delete?.min_age} isUsingRollover={isUsingRollover} hasDeletePhase={Boolean(formData._meta?.delete?.enabled)} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss index de49e665ed933..983ef0ab20f69 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -80,4 +80,12 @@ $ilmTimelineBarHeight: $euiSizeS; background-color: $euiColorVis1; } } + + &__frozenPhase { + width: var(--ilm-timeline-frozen-phase-width); + + &__colorBar { + background-color: $euiColorVis4; + } + } } 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 c996c45171d2f..8a0028dcb8b19 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 @@ -62,6 +62,9 @@ const i18nTexts = { coldPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.coldPhaseSectionTitle', { defaultMessage: 'Cold phase', }), + frozenPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.frozenPhaseSectionTitle', { + defaultMessage: 'Frozen phase', + }), deleteIcon: { toolTipContent: i18n.translate('xpack.indexLifecycleMgmt.timeline.deleteIconToolTipContent', { defaultMessage: 'Policy deletes the index after lifecycle phases complete.', @@ -84,12 +87,17 @@ const calculateWidths = (inputs: PhaseAgeInMilliseconds) => { inputs.phases.cold != null ? msTimeToOverallPercent(inputs.phases.cold, inputs.total) + SCORE_BUFFER_AMOUNT : 0; + const frozenScore = + inputs.phases.frozen != null + ? msTimeToOverallPercent(inputs.phases.frozen, inputs.total) + SCORE_BUFFER_AMOUNT + : 0; - const totalScore = hotScore + warmScore + coldScore; + const totalScore = hotScore + warmScore + coldScore + frozenScore; return { hot: `${toPercent(hotScore, totalScore)}%`, warm: `${toPercent(warmScore, totalScore)}%`, cold: `${toPercent(coldScore, totalScore)}%`, + frozen: `${toPercent(frozenScore, totalScore)}%`, }; }; @@ -102,6 +110,7 @@ interface Props { isUsingRollover: boolean; warmPhaseMinAge?: string; coldPhaseMinAge?: string; + frozenPhaseMinAge?: string; deletePhaseMinAge?: string; } @@ -115,6 +124,9 @@ export const Timeline: FunctionComponent = memo( hot: { min_age: phasesMinAge.hotPhaseMinAge }, warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined, cold: phasesMinAge.coldPhaseMinAge ? { min_age: phasesMinAge.coldPhaseMinAge } : undefined, + frozen: phasesMinAge.frozenPhaseMinAge + ? { min_age: phasesMinAge.frozenPhaseMinAge } + : undefined, delete: phasesMinAge.deletePhaseMinAge ? { min_age: phasesMinAge.deletePhaseMinAge } : undefined, @@ -157,6 +169,7 @@ export const Timeline: FunctionComponent = memo( el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot); el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null); el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null); + el.style.setProperty('--ilm-timeline-frozen-phase-width', widths.frozen ?? null); } }} > @@ -198,6 +211,18 @@ export const Timeline: FunctionComponent = memo( />
)} + {exists(phaseAgeInMilliseconds.phases.frozen) && ( +
+
+ +
+ )}
{hasDeletePhase && ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index befb8faf51aa1..ed165e8638843 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -43,6 +43,7 @@ import { ColdPhase, DeletePhase, HotPhase, + FrozenPhase, PolicyJsonFlyout, WarmPhase, Timeline, @@ -72,6 +73,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { policy: currentPolicy, existingPolicies, policyName, + license, } = useEditPolicyContext(); const serializer = useMemo(() => { @@ -80,6 +82,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { const [saveAsNew, setSaveAsNew] = useState(false); const originalPolicyName: string = isNewPolicy ? '' : policyName!; + const isAllowedByLicense = license.canUseSearchableSnapshot(); const { form } = useForm({ schema, @@ -243,13 +246,21 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { - - + {isAllowedByLicense && ( + <> + + + + )} + + {/* We can't add the here as it breaks the layout + and makes the connecting line go further that it needs to. + There is an issue in EUI to fix this (https://github.com/elastic/eui/issues/4492) */}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx index 7210dc6b7ce2b..34999368ffab2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx @@ -23,6 +23,7 @@ const isXPhaseField = (phase: keyof Phases) => (fieldPath: string): boolean => const isHotPhaseField = isXPhaseField('hot'); const isWarmPhaseField = isXPhaseField('warm'); const isColdPhaseField = isXPhaseField('cold'); +const isFrozenPhaseField = isXPhaseField('frozen'); const isDeletePhaseField = isXPhaseField('delete'); const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => { @@ -35,6 +36,9 @@ const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => { if (isColdPhaseField(fieldPath)) { return 'cold'; } + if (isFrozenPhaseField(fieldPath)) { + return 'frozen'; + } if (isDeletePhaseField(fieldPath)) { return 'delete'; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx index cc021f101cfb5..c2e55f7aa6e61 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -25,6 +25,7 @@ export interface ConfigurationIssues { * See https://github.com/elastic/elasticsearch/blob/master/docs/reference/ilm/actions/ilm-searchable-snapshot.asciidoc. */ isUsingSearchableSnapshotInHotPhase: boolean; + isUsingSearchableSnapshotInColdPhase: boolean; } const ConfigurationIssuesContext = createContext(null as any); @@ -32,10 +33,14 @@ const ConfigurationIssuesContext = createContext(null as an const pathToHotPhaseSearchableSnapshot = 'phases.hot.actions.searchable_snapshot.snapshot_repository'; +const pathToColdPhaseSearchableSnapshot = + 'phases.cold.actions.searchable_snapshot.snapshot_repository'; + export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ watch: [ pathToHotPhaseSearchableSnapshot, + pathToColdPhaseSearchableSnapshot, isUsingCustomRolloverPath, isUsingDefaultRolloverPath, ], @@ -50,6 +55,8 @@ export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => isUsingRollover: isUsingDefaultRollover === false ? isUsingCustomRollover : true, isUsingSearchableSnapshotInHotPhase: get(formData, pathToHotPhaseSearchableSnapshot) != null, + isUsingSearchableSnapshotInColdPhase: + get(formData, pathToColdPhaseSearchableSnapshot) != null, }} > {children} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 3e70cbb533653..227f135ca7b72 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -17,7 +17,7 @@ import { FormInternal } from '../types'; export const deserializer = (policy: SerializedPolicy): FormInternal => { const { - phases: { hot, warm, cold, delete: deletePhase }, + phases: { hot, warm, cold, frozen, delete: deletePhase }, } = policy; const _meta: FormInternal['_meta'] = { @@ -41,6 +41,11 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { dataTierAllocationType: determineDataTierAllocationType(cold?.actions), freezeEnabled: Boolean(cold?.actions?.freeze), }, + frozen: { + enabled: Boolean(frozen), + dataTierAllocationType: determineDataTierAllocationType(frozen?.actions), + freezeEnabled: Boolean(frozen?.actions?.freeze), + }, delete: { enabled: Boolean(deletePhase), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx index 9877a2ea9449c..b4aab0ffdea60 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx @@ -26,6 +26,7 @@ interface Errors { hot: ErrorGroup; warm: ErrorGroup; cold: ErrorGroup; + frozen: ErrorGroup; delete: ErrorGroup; /** * Errors that are not specific to a phase should go here. @@ -46,6 +47,7 @@ const createEmptyErrors = (): Errors => ({ hot: {}, warm: {}, cold: {}, + frozen: {}, delete: {}, other: {}, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 734a12a72bd30..6deb4d7fd4711 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -9,7 +9,7 @@ export { deserializer } from './deserializer'; export { createSerializer } from './serializer'; -export { schema } from './schema'; +export { schema, searchableSnapshotFields } from './schema'; export * from './validations'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx index 92cc8eeead91a..98ffb7e2dd7af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx @@ -23,19 +23,24 @@ const getPhaseTimingConfiguration = ( hot: PhaseTimingConfiguration; warm: PhaseTimingConfiguration; cold: PhaseTimingConfiguration; + frozen: PhaseTimingConfiguration; } => { const isWarmPhaseEnabled = formData?._meta?.warm?.enabled; const isColdPhaseEnabled = formData?._meta?.cold?.enabled; + const isFrozenPhaseEnabled = formData?._meta?.frozen?.enabled; + return { - hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled }, - warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled }, - cold: { isFinalDataPhase: isColdPhaseEnabled }, + hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled && !isFrozenPhaseEnabled }, + warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled && !isFrozenPhaseEnabled }, + cold: { isFinalDataPhase: isColdPhaseEnabled && !isFrozenPhaseEnabled }, + frozen: { isFinalDataPhase: isFrozenPhaseEnabled }, }; }; export interface PhaseTimings { hot: PhaseTimingConfiguration; warm: PhaseTimingConfiguration; cold: PhaseTimingConfiguration; + frozen: PhaseTimingConfiguration; isDeletePhaseEnabled: boolean; setDeletePhaseEnabled: (enabled: boolean) => void; } @@ -44,7 +49,12 @@ const PhaseTimingsContext = createContext(null as any); export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ - watch: ['_meta.warm.enabled', '_meta.cold.enabled', '_meta.delete.enabled'], + watch: [ + '_meta.warm.enabled', + '_meta.cold.enabled', + '_meta.frozen.enabled', + '_meta.delete.enabled', + ], }); return ( @@ -65,6 +75,7 @@ export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { ); }; + export const usePhaseTimings = () => { const ctx = useContext(PhaseTimingsContext); if (!ctx) throw new Error('Cannot use phase timings outside of phase timings context'); 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 65fc82b7ccc68..5861c7b320de1 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 @@ -30,6 +30,90 @@ const serializers = { stringToNumber: (v: string): any => (v != null ? parseInt(v, 10) : undefined), }; +const maxNumSegmentsField = { + label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', + { defaultMessage: 'A value for number of segments is required.' } + ) + ), + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: serializers.stringToNumber, +}; + +export const searchableSnapshotFields = { + snapshot_repository: { + label: i18nTexts.editPolicy.searchableSnapshotsRepoFieldLabel, + validations: [ + { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, + ], + }, + storage: { + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel', { + defaultMessage: 'Storage', + }), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageHelpText', + { + defaultMessage: + "Type of snapshot mounted for the searchable snapshot. This is an advanced option. Only change it if you know what you're doing.", + } + ), + }, +}; + +const numberOfReplicasField = { + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberOfReplicasLabel', { + defaultMessage: 'Number of replicas', + }), + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: ifExistsNumberNonNegative, + }, + ], + serializer: serializers.stringToNumber, +}; + +const numberOfShardsField = { + label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { + defaultMessage: 'Number of primary shards', + }), + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: numberGreaterThanField({ + message: i18nTexts.editPolicy.errors.numberGreatThan0Required, + than: 0, + }), + }, + ], + serializer: serializers.stringToNumber, +}; + +const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ + defaultValue: defaultIndexPriority[phase] as any, + label: i18nTexts.editPolicy.indexPriorityFieldLabel, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { validator: ifExistsNumberNonNegative }, + ], + serializer: serializers.stringToNumber, +}); + export const schema: FormSchema = { _meta: { hot: { @@ -110,6 +194,30 @@ export const schema: FormSchema = { label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, }, }, + frozen: { + enabled: { + defaultValue: false, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.activateFrozenPhaseSwitchLabel', + { defaultMessage: 'Activate frozen phase' } + ), + }, + freezeEnabled: { + defaultValue: false, + label: i18n.translate('xpack.indexLifecycleMgmt.frozePhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + }, + minAgeUnit: { + defaultValue: 'd', + }, + dataTierAllocationType: { + label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel, + }, + allocationNodeAttribute: { + label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, + }, + }, delete: { enabled: { defaultValue: false, @@ -172,55 +280,13 @@ export const schema: FormSchema = { }, }, forcemerge: { - max_num_segments: { - label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', - { defaultMessage: 'A value for number of segments is required.' } - ) - ), - }, - { - validator: ifExistsNumberGreaterThanZero, - }, - ], - serializer: serializers.stringToNumber, - }, + max_num_segments: maxNumSegmentsField, }, shrink: { - number_of_shards: { - label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { - defaultMessage: 'Number of primary shards', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: numberGreaterThanField({ - message: i18nTexts.editPolicy.errors.numberGreatThan0Required, - than: 0, - }), - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_shards: numberOfShardsField, }, set_priority: { - priority: { - defaultValue: defaultIndexPriority.hot as any, - label: i18nTexts.editPolicy.indexPriorityFieldLabel, - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { validator: ifExistsNumberNonNegative }, - ], - serializer: serializers.stringToNumber, - }, + priority: getPriorityField('hot'), }, }, }, @@ -235,71 +301,16 @@ export const schema: FormSchema = { }, actions: { allocate: { - number_of_replicas: { - label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel', { - defaultMessage: 'Number of replicas', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: ifExistsNumberNonNegative, - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_replicas: numberOfReplicasField, }, shrink: { - number_of_shards: { - label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { - defaultMessage: 'Number of primary shards', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: numberGreaterThanField({ - message: i18nTexts.editPolicy.errors.numberGreatThan0Required, - than: 0, - }), - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_shards: numberOfShardsField, }, forcemerge: { - max_num_segments: { - label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', - { defaultMessage: 'A value for number of segments is required.' } - ) - ), - }, - { - validator: ifExistsNumberGreaterThanZero, - }, - ], - serializer: serializers.stringToNumber, - }, + max_num_segments: maxNumSegmentsField, }, set_priority: { - priority: { - defaultValue: defaultIndexPriority.warm as any, - label: i18nTexts.editPolicy.indexPriorityFieldLabel, - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { validator: ifExistsNumberNonNegative }, - ], - serializer: serializers.stringToNumber, - }, + priority: getPriorityField('warm'), }, }, }, @@ -314,42 +325,31 @@ export const schema: FormSchema = { }, actions: { allocate: { - number_of_replicas: { - label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel', { - defaultMessage: 'Number of replicas', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: ifExistsNumberNonNegative, - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_replicas: numberOfReplicasField, }, set_priority: { - priority: { - defaultValue: defaultIndexPriority.cold as any, - label: i18nTexts.editPolicy.indexPriorityFieldLabel, - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { validator: ifExistsNumberNonNegative }, - ], - serializer: serializers.stringToNumber, - }, + priority: getPriorityField('cold'), }, - searchable_snapshot: { - snapshot_repository: { - label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, - validations: [ - { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, - ], + searchable_snapshot: searchableSnapshotFields, + }, + }, + frozen: { + min_age: { + defaultValue: '0', + validations: [ + { + validator: minAgeValidator, }, + ], + }, + actions: { + allocate: { + number_of_replicas: numberOfReplicasField, + }, + set_priority: { + priority: getPriorityField('frozen'), }, + searchable_snapshot: searchableSnapshotFields, }, }, delete: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 746ba4eeb801f..b21545ce1739c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -241,6 +241,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete draft.phases.cold; } + /** + * FROZEN PHASE SERIALIZATION + */ + if (_meta.frozen.enabled) { + draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {}; + const frozenPhase = draft.phases.frozen!; + + /** + * FROZEN PHASE SEARCHABLE SNAPSHOT + */ + if (!updatedPolicy.phases.frozen?.actions?.searchable_snapshot) { + delete frozenPhase.actions.searchable_snapshot; + } + } else { + delete draft.phases.frozen; + } + /** * DELETE PHASE SERIALIZATION */ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 1d75fb5031216..47585fba38768 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -77,12 +77,18 @@ export const i18nTexts = { defaultMessage: 'Select a node attribute', } ), - searchableSnapshotsFieldLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel', + searchableSnapshotsRepoFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel', { defaultMessage: 'Searchable snapshot repository', } ), + searchableSnapshotsStorageFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel', + { + defaultMessage: 'Searchable snapshot storage', + } + ), errors: { numberRequired: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage', @@ -188,6 +194,9 @@ export const i18nTexts = { cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle', { defaultMessage: 'Cold phase', }), + frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseTitle', { + defaultMessage: 'Frozen phase', + }), delete: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseTitle', { defaultMessage: 'Delete phase', }), @@ -205,6 +214,13 @@ export const i18nTexts = { defaultMessage: 'Move data to the cold tier, which is optimized for cost savings over search performance. Data is normally read-only in the cold phase.', }), + frozen: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseDescription', + { + defaultMessage: + 'Archive data as searchable snapshots in the frozen tier. The frozen tier is optimized for maximum cost savings. Data in the frozen tier is rarely accessed and never updated.', + } + ), delete: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescription', { 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 2974a88c22343..5d71bc057966e 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 @@ -28,11 +28,11 @@ import { FormInternal } from '../types'; /* -===- Private functions and types -===- */ -type MinAgePhase = 'warm' | 'cold' | 'delete'; +type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete'; type Phase = 'hot' | MinAgePhase; -const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; +const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'frozen', 'delete']; const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ min_age: formData.phases?.[phase]?.min_age @@ -69,6 +69,9 @@ export interface AbsoluteTimings { cold?: { min_age: string; }; + frozen?: { + min_age: string; + }; delete?: { min_age: string; }; @@ -80,6 +83,7 @@ export interface PhaseAgeInMilliseconds { hot: number; warm?: number; cold?: number; + frozen?: number; }; } @@ -92,6 +96,7 @@ export const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimin hot: { min_age: undefined }, warm: _meta.warm.enabled ? getMinAge('warm', formData) : undefined, cold: _meta.cold.enabled ? getMinAge('cold', formData) : undefined, + frozen: _meta.frozen?.enabled ? getMinAge('frozen', formData) : undefined, delete: _meta.delete.enabled ? getMinAge('delete', formData) : undefined, }; }; @@ -139,6 +144,7 @@ export const calculateRelativeFromAbsoluteMilliseconds = ( hot: 0, warm: inputs.warm ? 0 : undefined, cold: inputs.cold ? 0 : undefined, + frozen: inputs.frozen ? 0 : undefined, }, } ); 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 7aabf5d48e4fd..4330cde378b6d 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 @@ -53,6 +53,11 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { freezeEnabled: boolean; } +interface FrozenPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { + enabled: boolean; + freezeEnabled: boolean; +} + interface DeletePhaseMetaFields extends MinAgeField { enabled: boolean; } @@ -69,6 +74,7 @@ export interface FormInternal extends SerializedPolicy { hot: HotPhaseMetaFields; warm: WarmPhaseMetaFields; cold: ColdPhaseMetaFields; + frozen: FrozenPhaseMetaFields; delete: DeletePhaseMetaFields; }; } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts index b46b77f2776c9..accd8993abc62 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -75,6 +75,7 @@ export function registerListRoute({ 'ml.enabled', 'ml.machine_memory', 'ml.max_open_jobs', + 'ml.max_jvm_size', // Used by ML to identify nodes that have transform enabled: // https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147 'transform.node', diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 1cca7d29874d6..7a4795f8e370b 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -37,6 +37,7 @@ const bodySchema = schema.object({ hot: schema.any(), warm: schema.maybe(schema.any()), cold: schema.maybe(schema.any()), + frozen: schema.maybe(schema.any()), delete: schema.maybe(schema.any()), }), }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap index 992301af13ad0..afc69c2e8861f 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -537,7 +537,7 @@ exports[`DatatableComponent it should not render actions on header when it is in Array [ Object { "actions": Object { - "additional": undefined, + "additional": Array [], "showHide": false, "showMoveLeft": false, "showMoveRight": false, @@ -551,7 +551,7 @@ exports[`DatatableComponent it should not render actions on header when it is in }, Object { "actions": Object { - "additional": undefined, + "additional": Array [], "showHide": false, "showMoveLeft": false, "showMoveRight": false, @@ -565,7 +565,7 @@ exports[`DatatableComponent it should not render actions on header when it is in }, Object { "actions": Object { - "additional": undefined, + "additional": Array [], "showHide": false, "showMoveLeft": false, "showMoveRight": false, diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx index fdb05599c38e9..ba24da8309ed7 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx @@ -7,8 +7,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; -import type { Datatable, DatatableColumnMeta } from 'src/plugins/expressions'; +import { + EuiDataGridColumn, + EuiDataGridColumnCellActionProps, + EuiListGroupItemProps, +} from '@elastic/eui'; +import type { Datatable, DatatableColumn, DatatableColumnMeta } from 'src/plugins/expressions'; import type { FormatFactory } from '../../types'; import { ColumnConfig } from './table_basic'; @@ -22,6 +26,10 @@ export const createGridColumns = ( rowIndex: number, negate?: boolean ) => void, + handleTransposedColumnClick: ( + bucketValues: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>, + negate?: boolean + ) => void, isReadOnly: boolean, columnConfig: ColumnConfig, visibleColumns: string[], @@ -135,9 +143,63 @@ export const createGridColumns = ( ] : undefined; - const column = columnConfig.columns.find(({ columnId }) => columnId === field); - const initialWidth = column?.width; - const isHidden = column?.hidden; + const columnArgs = columnConfig.columns.find(({ columnId }) => columnId === field); + const isTransposed = Boolean(columnArgs?.originalColumnId); + const initialWidth = columnArgs?.width; + const isHidden = columnArgs?.hidden; + const originalColumnId = columnArgs?.originalColumnId; + + const additionalActions: EuiListGroupItemProps[] = []; + + if (!isReadOnly) { + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }), + iconType: 'empty', + label: i18n.translate('xpack.lens.table.resize.reset', { + defaultMessage: 'Reset width', + }), + 'data-test-subj': 'lensDatatableResetWidth', + isDisabled: initialWidth == null, + }); + if (!isTransposed) { + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => onColumnHide({ columnId: originalColumnId || field }), + iconType: 'eyeClosed', + label: i18n.translate('xpack.lens.table.hide.hideLabel', { + defaultMessage: 'Hide', + }), + 'data-test-subj': 'lensDatatableHide', + isDisabled: !isHidden && visibleColumns.length <= 1, + }); + } else if (columnArgs?.bucketValues) { + const bucketValues = columnArgs?.bucketValues; + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => handleTransposedColumnClick(bucketValues, false), + iconType: 'plusInCircle', + label: i18n.translate('xpack.lens.table.columnFilter.filterForValueText', { + defaultMessage: 'Filter for column', + }), + 'data-test-subj': 'lensDatatableHide', + }); + + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => handleTransposedColumnClick(bucketValues, true), + iconType: 'minusInCircle', + label: i18n.translate('xpack.lens.table.columnFilter.filterOutValueText', { + defaultMessage: 'Filter out column', + }), + 'data-test-subj': 'lensDatatableHide', + }); + } + } const columnDefinition: EuiDataGridColumn = { id: field, @@ -162,32 +224,7 @@ export const createGridColumns = ( defaultMessage: 'Sort descending', }), }, - additional: isReadOnly - ? undefined - : [ - { - color: 'text', - size: 'xs', - onClick: () => onColumnResize({ columnId: field, width: undefined }), - iconType: 'empty', - label: i18n.translate('xpack.lens.table.resize.reset', { - defaultMessage: 'Reset width', - }), - 'data-test-subj': 'lensDatatableResetWidth', - isDisabled: initialWidth == null, - }, - { - color: 'text', - size: 'xs', - onClick: () => onColumnHide({ columnId: field }), - iconType: 'eyeClosed', - label: i18n.translate('xpack.lens.table.hide.hideLabel', { - defaultMessage: 'Hide', - }), - 'data-test-subj': 'lensDatatableHide', - isDisabled: !isHidden && visibleColumns.length <= 1, - }, - ], + additional: additionalActions, }, }; 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 9c60cd47af3e3..672b29846d760 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 @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; import { VisualizationDimensionEditorProps } from '../../types'; import { DatatableVisualizationState } from '../visualization'; +import { getOriginalId } from '../transpose_helpers'; const idPrefix = htmlIdGenerator()(); @@ -20,13 +21,15 @@ export function TableDimensionEditor( const column = state.columns.find(({ columnId }) => accessor === columnId); if (!column) return null; + if (column.isTransposed) return null; // either read config state or use same logic as chart itself const currentAlignment = column?.alignment || (frame.activeData && - frame.activeData[state.layerId].columns.find((col) => col.id === accessor)?.meta.type === - 'number' + frame.activeData[state.layerId].columns.find( + (col) => col.id === accessor || getOriginalId(col.id) === accessor + )?.meta.type === 'number' ? 'right' : 'left'); @@ -89,39 +92,41 @@ export function TableDimensionEditor( }} /> - + display="columnCompressedSwitch" + > + { + const newState = { + ...state, + columns: state.columns.map((currentColumn) => { + if (currentColumn.columnId === accessor) { + return { + ...currentColumn, + hidden: !column.hidden, + }; + } else { + return currentColumn; + } + }), + }; + setState(newState); + }} + /> + + )} ); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts index 68416ac9a60aa..8490d33f83444 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts @@ -15,9 +15,11 @@ import { createGridResizeHandler, createGridSortingConfig, createGridHideHandler, + createTransposeColumnFilterHandler, } from './table_actions'; import { LensGridDirection } from './types'; import { ColumnConfig } from './table_basic'; +import { LensMultiTable } from '../../types'; function getDefaultConfig(): ColumnConfig { return { @@ -48,6 +50,19 @@ function createTableRef( }; } +function createUntransposedRef(options?: { + withDate: boolean; +}): React.MutableRefObject { + return { + current: { + type: 'lens_multitable', + tables: { + first: createTableRef(options).current, + }, + }, + }; +} + describe('Table actions', () => { const onEditAction = jest.fn(); @@ -132,6 +147,138 @@ describe('Table actions', () => { }); }); }); + + describe('Transposed column filtering', () => { + it('should set a filter on click with the correct configuration', () => { + const onClickValue = jest.fn(); + const tableRef = createUntransposedRef({ withDate: true }); + tableRef.current.tables.first.rows = [{ a: 123456 }]; + const filterHandle = createTransposeColumnFilterHandler(onClickValue, tableRef); + + filterHandle( + [ + { + originalBucketColumn: tableRef.current.tables.first.columns[0], + value: 123456, + }, + ], + false + ); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current.tables.first, + value: 123456, + }, + ], + negate: false, + timeFieldName: 'a', + }); + }); + + it('should set a negate filter on click with the correct configuration', () => { + const onClickValue = jest.fn(); + const tableRef = createUntransposedRef({ withDate: true }); + tableRef.current.tables.first.rows = [{ a: 123456 }]; + const filterHandle = createTransposeColumnFilterHandler(onClickValue, tableRef); + + filterHandle( + [ + { + originalBucketColumn: tableRef.current.tables.first.columns[0], + value: 123456, + }, + ], + true + ); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current.tables.first, + value: 123456, + }, + ], + negate: true, + timeFieldName: undefined, + }); + }); + + it('should set a multi filter and look up positions of the values', () => { + const onClickValue = jest.fn(); + const tableRef = createUntransposedRef({ withDate: false }); + const filterHandle = createTransposeColumnFilterHandler(onClickValue, tableRef); + tableRef.current.tables.first.columns = [ + { + id: 'a', + name: 'a', + meta: { + type: 'string', + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'string', + }, + }, + ]; + tableRef.current.tables.first.rows = [ + { + a: 'a1', + b: 'b1', + }, + { + a: 'a2', + b: 'b2', + }, + { + a: 'a3', + b: 'b3', + }, + { + a: 'a4', + b: 'b4', + }, + ]; + + filterHandle( + [ + { + originalBucketColumn: tableRef.current.tables.first.columns[0], + value: 'a2', + }, + { + originalBucketColumn: tableRef.current.tables.first.columns[1], + value: 'b3', + }, + ], + false + ); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 1, + table: tableRef.current.tables.first, + value: 'a2', + }, + { + column: 1, + row: 2, + table: tableRef.current.tables.first, + value: 'b3', + }, + ], + negate: false, + timeFieldName: undefined, + }); + }); + }); describe('Table sorting', () => { it('should create the right configuration for all types of sorting', () => { const configs: Array<{ diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts index 4f0271b758ffb..0d44ae3aa6dec 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -6,8 +6,8 @@ */ import type { EuiDataGridSorting } from '@elastic/eui'; -import type { Datatable } from 'src/plugins/expressions'; -import type { LensFilterEvent } from '../../types'; +import type { Datatable, DatatableColumn } from 'src/plugins/expressions'; +import type { LensFilterEvent, LensMultiTable } from '../../types'; import type { LensGridDirection, LensResizeAction, @@ -17,18 +17,20 @@ import type { import { ColumnConfig } from './table_basic'; import { desanitizeFilterContext } from '../../utils'; +import { getOriginalId } from '../transpose_helpers'; export const createGridResizeHandler = ( columnConfig: ColumnConfig, setColumnConfig: React.Dispatch>, onEditAction: (data: LensResizeAction['data']) => void ) => (eventData: { columnId: string; width: number | undefined }) => { + const originalColumnId = getOriginalId(eventData.columnId); // directly set the local state of the component to make sure the visualization re-renders immediately, // re-layouting and taking up all of the available space. setColumnConfig({ ...columnConfig, columns: columnConfig.columns.map((column) => { - if (column.columnId === eventData.columnId) { + if (column.columnId === eventData.columnId || column.originalColumnId === originalColumnId) { return { ...column, width: eventData.width }; } return column; @@ -36,7 +38,7 @@ export const createGridResizeHandler = ( }); return onEditAction({ action: 'resize', - columnId: eventData.columnId, + columnId: originalColumnId, width: eventData.width, }); }; @@ -46,11 +48,12 @@ export const createGridHideHandler = ( setColumnConfig: React.Dispatch>, onEditAction: (data: LensToggleAction['data']) => void ) => (eventData: { columnId: string }) => { + const originalColumnId = getOriginalId(eventData.columnId); // directly set the local state of the component to make sure the visualization re-renders immediately setColumnConfig({ ...columnConfig, columns: columnConfig.columns.map((column) => { - if (column.columnId === eventData.columnId) { + if (column.columnId === eventData.columnId || column.originalColumnId === originalColumnId) { return { ...column, hidden: true }; } return column; @@ -58,7 +61,7 @@ export const createGridHideHandler = ( }); return onEditAction({ action: 'toggle', - columnId: eventData.columnId, + columnId: originalColumnId, }); }; @@ -92,6 +95,39 @@ export const createGridFilterHandler = ( onClickValue(desanitizeFilterContext(data)); }; +export const createTransposeColumnFilterHandler = ( + onClickValue: (data: LensFilterEvent['data']) => void, + untransposedDataRef: React.MutableRefObject +) => ( + bucketValues: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>, + negate: boolean = false +) => { + if (!untransposedDataRef.current) return; + const originalTable = Object.values(untransposedDataRef.current.tables)[0]; + const timeField = bucketValues.find( + ({ originalBucketColumn }) => originalBucketColumn.meta.type === 'date' + )?.originalBucketColumn; + const isDate = Boolean(timeField); + const timeFieldName = negate && isDate ? undefined : timeField?.meta?.field; + + const data: LensFilterEvent['data'] = { + negate, + data: bucketValues.map(({ originalBucketColumn, value }) => { + const columnIndex = originalTable.columns.findIndex((c) => c.id === originalBucketColumn.id); + const rowIndex = originalTable.rows.findIndex((r) => r[originalBucketColumn.id] === value); + return { + row: rowIndex, + column: columnIndex, + value, + table: originalTable, + }; + }), + timeFieldName, + }; + + onClickValue(desanitizeFilterContext(data)); +}; + export const createGridSortingConfig = ( sortBy: string | undefined, sortDirection: LensGridDirection, 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 e1687ba28f07b..24cde07cebaa0 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 @@ -38,6 +38,7 @@ import { createGridHideHandler, createGridResizeHandler, createGridSortingConfig, + createTransposeColumnFilterHandler, } from './table_actions'; export const DataContext = React.createContext({}); @@ -82,6 +83,9 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const firstTableRef = useRef(firstLocalTable); firstTableRef.current = firstLocalTable; + const untransposedDataRef = useRef(props.untransposedData); + untransposedDataRef.current = props.untransposedData; + const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions?.some((x) => x); const { getType, dispatchEvent, renderMode, formatFactory } = props; @@ -125,6 +129,11 @@ export const DatatableComponent = (props: DatatableRenderProps) => { onClickValue, ]); + const handleTransposedColumnClick = useMemo( + () => createTransposeColumnFilterHandler(onClickValue, untransposedDataRef), + [onClickValue, untransposedDataRef] + ); + const bucketColumns = useMemo( () => columnConfig.columns @@ -172,6 +181,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { bucketColumns, firstLocalTable, handleFilterClick, + handleTransposedColumnClick, isReadOnlySorted, columnConfig, visibleColumns, @@ -183,6 +193,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { bucketColumns, firstLocalTable, handleFilterClick, + handleTransposedColumnClick, isReadOnlySorted, columnConfig, visibleColumns, diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index 3ee41d4e9aeed..3ba448b49afc9 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -73,7 +73,7 @@ function sampleArgs() { type: 'lens_datatable_column', }, ], - sortingColumnId: '', + sortingColumnId: undefined, sortingDirection: 'none', }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index f6a38541cda27..7d879217abf8b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -7,11 +7,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import type { IAggType } from 'src/plugins/data/public'; -import type { +import { DatatableColumnMeta, ExpressionFunctionDefinition, ExpressionRenderDefinition, @@ -23,8 +24,9 @@ import { ColumnState } from './visualization'; import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } from '../types'; import type { DatatableRender } from './components/types'; +import { transposeTable } from './transpose_helpers'; -interface Args { +export interface Args { title: string; description?: string; columns: Array; @@ -34,6 +36,7 @@ interface Args { export interface DatatableProps { data: LensMultiTable; + untransposedData?: LensMultiTable; args: Args; } @@ -78,6 +81,7 @@ export const getDatatable = ({ }, }, fn(data, args, context) { + let untransposedData: LensMultiTable | undefined; // do the sorting at this level to propagate it also at CSV download const [firstTable] = Object.values(data.tables); const [layerId] = Object.keys(context.inspectorAdapters.tables || {}); @@ -86,6 +90,15 @@ export const getDatatable = ({ firstTable.columns.forEach((column) => { formatters[column.id] = formatFactory(column.meta?.params); }); + + const hasTransposedColumns = args.columns.some((c) => c.isTransposed); + if (hasTransposedColumns) { + // store original shape of data separately + untransposedData = cloneDeep(data); + // transposes table and args inplace + transposeTable(args, firstTable, formatters); + } + const { sortingColumnId: sortBy, sortingDirection: sortDirection } = args; const columnsReverseLookup = firstTable.columns.reduce< @@ -95,7 +108,7 @@ export const getDatatable = ({ return memo; }, {}); - if (sortBy && sortDirection !== 'none') { + if (sortBy && columnsReverseLookup[sortBy] && sortDirection !== 'none') { // Sort on raw values for these types, while use the formatted value for the rest const sortingCriteria = getSortingCriteria( isRange(columnsReverseLookup[sortBy]?.meta) @@ -111,12 +124,16 @@ export const getDatatable = ({ .sort(sortingCriteria); // replace also the local copy firstTable.rows = context.inspectorAdapters.tables[layerId].rows; + } else { + args.sortingColumnId = undefined; + args.sortingDirection = 'none'; } return { type: 'render', as: 'lens_datatable_renderer', value: { data, + untransposedData, args, }, }; @@ -141,6 +158,8 @@ export const datatableColumn: ExpressionFunctionDefinition< alignment: { types: ['string'], help: '' }, hidden: { types: ['boolean'], help: '' }, width: { types: ['number'], help: '' }, + isTransposed: { types: ['boolean'], help: '' }, + transposable: { types: ['boolean'], help: '' }, }, fn: function fn(input: unknown, args: ColumnState) { return { diff --git a/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts new file mode 100644 index 0000000000000..91559a1778f4f --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts @@ -0,0 +1,293 @@ +/* + * 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 { FieldFormat } from 'src/plugins/data/public'; +import type { Datatable } from 'src/plugins/expressions'; + +import { Args } from './expression'; +import { transposeTable } from './transpose_helpers'; + +describe('transpose_helpes', () => { + function buildTable(): Datatable { + // 3 buckets, 2 metrics + // first bucket goes A/B/C + // second buckets goes D/E/F + // third bucket goes X/Y/Z (all combinations) + // metric values count up from 1 + return { + type: 'datatable', + columns: [ + { id: 'bucket1', name: 'bucket1', meta: { type: 'string' } }, + { id: 'bucket2', name: 'bucket2', meta: { type: 'string' } }, + { id: 'bucket3', name: 'bucket3', meta: { type: 'string' } }, + { id: 'metric1', name: 'metric1', meta: { type: 'number' } }, + { id: 'metric2', name: 'metric2', meta: { type: 'number' } }, + ], + rows: [ + { bucket1: 'A', bucket2: 'D', bucket3: 'X', metric1: 1, metric2: 2 }, + { bucket1: 'A', bucket2: 'D', bucket3: 'Y', metric1: 3, metric2: 4 }, + { bucket1: 'A', bucket2: 'D', bucket3: 'Z', metric1: 5, metric2: 6 }, + { bucket1: 'A', bucket2: 'E', bucket3: 'X', metric1: 7, metric2: 8 }, + { bucket1: 'A', bucket2: 'E', bucket3: 'Y', metric1: 9, metric2: 10 }, + { bucket1: 'A', bucket2: 'E', bucket3: 'Z', metric1: 11, metric2: 12 }, + { bucket1: 'A', bucket2: 'F', bucket3: 'X', metric1: 13, metric2: 14 }, + { bucket1: 'A', bucket2: 'F', bucket3: 'Y', metric1: 15, metric2: 16 }, + { bucket1: 'A', bucket2: 'F', bucket3: 'Z', metric1: 17, metric2: 18 }, + { bucket1: 'B', bucket2: 'D', bucket3: 'X', metric1: 19, metric2: 20 }, + { bucket1: 'B', bucket2: 'D', bucket3: 'Y', metric1: 21, metric2: 22 }, + { bucket1: 'B', bucket2: 'D', bucket3: 'Z', metric1: 23, metric2: 24 }, + { bucket1: 'B', bucket2: 'E', bucket3: 'X', metric1: 25, metric2: 26 }, + { bucket1: 'B', bucket2: 'E', bucket3: 'Y', metric1: 27, metric2: 28 }, + { bucket1: 'B', bucket2: 'E', bucket3: 'Z', metric1: 29, metric2: 30 }, + { bucket1: 'B', bucket2: 'F', bucket3: 'X', metric1: 31, metric2: 32 }, + { bucket1: 'B', bucket2: 'F', bucket3: 'Y', metric1: 33, metric2: 34 }, + { bucket1: 'B', bucket2: 'F', bucket3: 'Z', metric1: 35, metric2: 36 }, + { bucket1: 'C', bucket2: 'D', bucket3: 'X', metric1: 37, metric2: 38 }, + { bucket1: 'C', bucket2: 'D', bucket3: 'Y', metric1: 39, metric2: 40 }, + { bucket1: 'C', bucket2: 'D', bucket3: 'Z', metric1: 41, metric2: 42 }, + { bucket1: 'C', bucket2: 'E', bucket3: 'X', metric1: 43, metric2: 44 }, + { bucket1: 'C', bucket2: 'E', bucket3: 'Y', metric1: 45, metric2: 46 }, + { bucket1: 'C', bucket2: 'E', bucket3: 'Z', metric1: 47, metric2: 48 }, + { bucket1: 'C', bucket2: 'F', bucket3: 'X', metric1: 49, metric2: 50 }, + { bucket1: 'C', bucket2: 'F', bucket3: 'Y', metric1: 51, metric2: 52 }, + { bucket1: 'C', bucket2: 'F', bucket3: 'Z', metric1: 53, metric2: 54 }, + ], + }; + } + + function buildArgs(): Args { + return { + title: 'Table', + sortingColumnId: undefined, + sortingDirection: 'none', + columns: [ + { + type: 'lens_datatable_column', + columnId: 'bucket1', + isTransposed: false, + transposable: false, + }, + { + type: 'lens_datatable_column', + columnId: 'bucket2', + isTransposed: false, + transposable: false, + }, + { + type: 'lens_datatable_column', + columnId: 'bucket3', + isTransposed: false, + transposable: false, + }, + { + type: 'lens_datatable_column', + columnId: 'metric1', + isTransposed: false, + transposable: true, + }, + { + type: 'lens_datatable_column', + columnId: 'metric2', + isTransposed: false, + transposable: true, + }, + ], + }; + } + + function buildFormatters() { + return ({ + bucket1: { convert: (x: unknown) => x }, + bucket2: { convert: (x: unknown) => x }, + bucket3: { convert: (x: unknown) => x }, + metric1: { convert: (x: unknown) => x }, + metric2: { convert: (x: unknown) => x }, + } as unknown) as Record; + } + + it('should transpose table by one column', () => { + const table = buildTable(); + const args = buildArgs(); + args.columns[0].isTransposed = true; + transposeTable(args, table, buildFormatters()); + + // one metric for each unique value of bucket1 + expect(table.columns.map((c) => c.id)).toEqual([ + 'bucket2', + 'bucket3', + 'A---metric1', + 'B---metric1', + 'C---metric1', + 'A---metric2', + 'B---metric2', + 'C---metric2', + ]); + + // order is different for args to visually group unique values + const expectedColumns = [ + 'bucket2', + 'bucket3', + 'A---metric1', + 'A---metric2', + 'B---metric1', + 'B---metric2', + 'C---metric1', + 'C---metric2', + ]; + + // args should be in sync + expect(args.columns.map((c) => c.columnId)).toEqual(expectedColumns); + // original column id should stay preserved + expect(args.columns.slice(2).map((c) => c.originalColumnId)).toEqual([ + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + ]); + + // data should stay consistent + expect(table.rows.length).toEqual(9); + table.rows.forEach((row, index) => { + expect(row['A---metric1']).toEqual(index * 2 + 1); + expect(row['A---metric2']).toEqual(index * 2 + 2); + // B metrics start with offset 18 because there are 18 A metrics (2 metrics * 3 bucket2 metrics * 3 bucket3 metrics) + expect(row['B---metric1']).toEqual(18 + index * 2 + 1); + expect(row['B---metric2']).toEqual(18 + index * 2 + 2); + // B metrics start with offset 36 because there are 18 A metrics and 18 B metrics (2 metrics * 3 bucket2 values * 3 bucket3 values) + expect(row['C---metric1']).toEqual(36 + index * 2 + 1); + expect(row['C---metric2']).toEqual(36 + index * 2 + 2); + }); + + // visible name should use separator + expect(table.columns[2].name).toEqual(`A › metric1`); + }); + + it('should transpose table by two columns', () => { + const table = buildTable(); + const args = buildArgs(); + args.columns[0].isTransposed = true; + args.columns[1].isTransposed = true; + transposeTable(args, table, buildFormatters()); + + // one metric for each unique value of bucket1 + expect(table.columns.map((c) => c.id)).toEqual([ + 'bucket3', + 'A---D---metric1', + 'B---D---metric1', + 'C---D---metric1', + 'A---E---metric1', + 'B---E---metric1', + 'C---E---metric1', + 'A---F---metric1', + 'B---F---metric1', + 'C---F---metric1', + 'A---D---metric2', + 'B---D---metric2', + 'C---D---metric2', + 'A---E---metric2', + 'B---E---metric2', + 'C---E---metric2', + 'A---F---metric2', + 'B---F---metric2', + 'C---F---metric2', + ]); + + // order is different for args to visually group unique values + const expectedColumns = [ + 'bucket3', + 'A---D---metric1', + 'A---D---metric2', + 'A---E---metric1', + 'A---E---metric2', + 'A---F---metric1', + 'A---F---metric2', + 'B---D---metric1', + 'B---D---metric2', + 'B---E---metric1', + 'B---E---metric2', + 'B---F---metric1', + 'B---F---metric2', + 'C---D---metric1', + 'C---D---metric2', + 'C---E---metric1', + 'C---E---metric2', + 'C---F---metric1', + 'C---F---metric2', + ]; + + // args should be in sync + expect(args.columns.map((c) => c.columnId)).toEqual(expectedColumns); + // original column id should stay preserved + expect(args.columns.slice(1).map((c) => c.originalColumnId)).toEqual([ + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + ]); + + // data should stay consistent + expect(table.rows.length).toEqual(3); + table.rows.forEach((row, index) => { + // each metric block has an additional offset of 6 because there are 6 metrics for each bucket1/bucket2 combination (2 metrics * 3 bucket3 values) + expect(row['A---D---metric1']).toEqual(index * 2 + 1); + expect(row['A---D---metric2']).toEqual(index * 2 + 2); + expect(row['A---E---metric1']).toEqual(index * 2 + 6 + 1); + expect(row['A---E---metric2']).toEqual(index * 2 + 6 + 2); + expect(row['A---F---metric1']).toEqual(index * 2 + 12 + 1); + expect(row['A---F---metric2']).toEqual(index * 2 + 12 + 2); + + expect(row['B---D---metric1']).toEqual(index * 2 + 18 + 1); + expect(row['B---D---metric2']).toEqual(index * 2 + 18 + 2); + expect(row['B---E---metric1']).toEqual(index * 2 + 24 + 1); + expect(row['B---E---metric2']).toEqual(index * 2 + 24 + 2); + expect(row['B---F---metric1']).toEqual(index * 2 + 30 + 1); + expect(row['B---F---metric2']).toEqual(index * 2 + 30 + 2); + + expect(row['C---D---metric1']).toEqual(index * 2 + 36 + 1); + expect(row['C---D---metric2']).toEqual(index * 2 + 36 + 2); + expect(row['C---E---metric1']).toEqual(index * 2 + 42 + 1); + expect(row['C---E---metric2']).toEqual(index * 2 + 42 + 2); + expect(row['C---F---metric1']).toEqual(index * 2 + 48 + 1); + expect(row['C---F---metric2']).toEqual(index * 2 + 48 + 2); + }); + }); + + it('should be able to handle missing values', () => { + const table = buildTable(); + const args = buildArgs(); + args.columns[0].isTransposed = true; + args.columns[1].isTransposed = true; + // delete A-E-Z bucket + table.rows.splice(5, 1); + transposeTable(args, table, buildFormatters()); + expect(args.columns.length).toEqual(19); + expect(table.columns.length).toEqual(19); + expect(table.rows.length).toEqual(3); + expect(table.rows[2]['A---E---metric1']).toEqual(undefined); + expect(table.rows[2]['A---E---metric2']).toEqual(undefined); + // 1 bucket column and 2 missing from the regular 18 columns + expect(Object.values(table.rows[2]).filter((val) => val !== undefined).length).toEqual( + 1 + 18 - 2 + ); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts new file mode 100644 index 0000000000000..6e29e018b481e --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts @@ -0,0 +1,237 @@ +/* + * 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 { FieldFormat } from 'src/plugins/data/public'; +import type { Datatable, DatatableColumn, DatatableRow } from 'src/plugins/expressions'; + +import { Args } from './expression'; +import { ColumnState } from './visualization'; + +const TRANSPOSE_SEPARATOR = '---'; + +const TRANSPOSE_VISUAL_SEPARATOR = '›'; + +export function getTransposeId(value: string, columnId: string) { + return `${value}${TRANSPOSE_SEPARATOR}${columnId}`; +} + +export function getOriginalId(id: string) { + if (id.includes(TRANSPOSE_SEPARATOR)) { + const idParts = id.split(TRANSPOSE_SEPARATOR); + return idParts[idParts.length - 1]; + } + return id; +} + +/** + * Transposes the columns of the given table as defined in the arguments. + * This function modifies the passed in args and firstTable objects. + * This process consists out of three parts: + * * Calculating the new column arguments + * * Calculating the new datatable columns + * * Calculating the new rows + * + * If the table is tranposed by multiple columns, this process is repeated on top of the previous transformation. + * + * @param args Arguments for the table visualization + * @param firstTable datatable object containing the actual data + * @param formatters Formatters for all columns to transpose columns by actual display values + */ +export function transposeTable( + args: Args, + firstTable: Datatable, + formatters: Record +) { + args.columns + .filter((columnArgs) => columnArgs.isTransposed) + // start with the inner nested transposed column and work up to preserve column grouping + .reverse() + .forEach(({ columnId: transposedColumnId }) => { + const datatableColumnIndex = firstTable.columns.findIndex((c) => c.id === transposedColumnId); + const datatableColumn = firstTable.columns[datatableColumnIndex]; + const transposedColumnFormatter = formatters[datatableColumn.id]; + const { uniqueValues, uniqueRawValues } = getUniqueValues( + firstTable, + transposedColumnFormatter, + transposedColumnId + ); + const metricsColumnArgs = args.columns.filter((c) => c.transposable); + const bucketsColumnArgs = args.columns.filter( + (c) => !c.transposable && c.columnId !== transposedColumnId + ); + firstTable.columns.splice(datatableColumnIndex, 1); + + transposeColumns( + args, + bucketsColumnArgs, + metricsColumnArgs, + firstTable, + uniqueValues, + uniqueRawValues, + datatableColumn + ); + transposeRows( + firstTable, + bucketsColumnArgs, + formatters, + transposedColumnFormatter, + transposedColumnId, + metricsColumnArgs + ); + }); +} + +function transposeRows( + firstTable: Datatable, + bucketsColumnArgs: Array, + formatters: Record, + transposedColumnFormatter: FieldFormat, + transposedColumnId: string, + metricsColumnArgs: Array +) { + const rowsByBucketColumns: Record = groupRowsByBucketColumns( + firstTable, + bucketsColumnArgs, + formatters + ); + firstTable.rows = mergeRowGroups( + rowsByBucketColumns, + bucketsColumnArgs, + transposedColumnFormatter, + transposedColumnId, + metricsColumnArgs + ); +} + +/** + * Updates column args by adding bucket column args first, then adding transposed metric columns + * grouped by unique value + */ +function updateColumnArgs( + args: Args, + bucketsColumnArgs: Array, + transposedColumnGroups: Array> +) { + args.columns = [...bucketsColumnArgs]; + // add first column from each group, then add second column for each group, ... + transposedColumnGroups[0].forEach((_, index) => { + transposedColumnGroups.forEach((transposedColumnGroup) => { + args.columns.push(transposedColumnGroup[index]); + }); + }); +} + +/** + * Finds all unique values in a column in order of first occurence + * @param table Table to search through + * @param formatter formatter for the column + * @param columnId column + */ +function getUniqueValues(table: Datatable, formatter: FieldFormat, columnId: string) { + const values = new Map(); + table.rows.forEach((row) => { + const rawValue = row[columnId]; + values.set(formatter.convert(row[columnId]), rawValue); + }); + const uniqueValues = [...values.keys()]; + const uniqueRawValues = [...values.values()]; + return { uniqueValues, uniqueRawValues }; +} + +/** + * Calculate transposed column objects of the datatable object and puts them into the datatable. + * Returns args for additional columns grouped by metric + * @param metricColumns + * @param firstTable + * @param uniqueValues + */ +function transposeColumns( + args: Args, + bucketsColumnArgs: Array, + metricColumns: Array, + firstTable: Datatable, + uniqueValues: string[], + uniqueRawValues: unknown[], + transposingDatatableColumn: DatatableColumn +) { + const columnGroups = metricColumns.map((metricColumn) => { + const originalDatatableColumn = firstTable.columns.find((c) => c.id === metricColumn.columnId)!; + const datatableColumns = uniqueValues.map((uniqueValue) => { + return { + ...originalDatatableColumn, + id: getTransposeId(uniqueValue, metricColumn.columnId), + name: `${uniqueValue} ${TRANSPOSE_VISUAL_SEPARATOR} ${originalDatatableColumn.name}`, + }; + }); + firstTable.columns.splice( + firstTable.columns.findIndex((c) => c.id === metricColumn.columnId), + 1, + ...datatableColumns + ); + return uniqueValues.map((uniqueValue, valueIndex) => { + return { + ...metricColumn, + columnId: getTransposeId(uniqueValue, metricColumn.columnId), + originalColumnId: metricColumn.originalColumnId || metricColumn.columnId, + originalName: metricColumn.originalName || originalDatatableColumn.name, + bucketValues: [ + ...(metricColumn.bucketValues || []), + { + originalBucketColumn: transposingDatatableColumn, + value: uniqueRawValues[valueIndex], + }, + ], + }; + }); + }); + updateColumnArgs(args, bucketsColumnArgs, columnGroups); +} + +/** + * Merge groups of rows together by creating separate columns for unique values of the column to transpose by. + */ +function mergeRowGroups( + rowsByBucketColumns: Record, + bucketColumns: ColumnState[], + formatter: FieldFormat, + transposedColumnId: string, + metricColumns: ColumnState[] +) { + return Object.values(rowsByBucketColumns).map((rows) => { + const mergedRow: DatatableRow = {}; + bucketColumns.forEach((c) => { + mergedRow[c.columnId] = rows[0][c.columnId]; + }); + rows.forEach((row) => { + const transposalValue = formatter.convert(row[transposedColumnId]); + metricColumns.forEach((c) => { + mergedRow[getTransposeId(transposalValue, c.columnId)] = row[c.columnId]; + }); + }); + return mergedRow; + }); +} + +/** + * Groups rows of the data table by the values of bucket columns which are not transposed by. + * All rows ending up in a group have the same bucket column value, but have different values of the column to transpose by. + */ +function groupRowsByBucketColumns( + firstTable: Datatable, + bucketColumns: ColumnState[], + formatters: Record +) { + const rowsByBucketColumns: Record = {}; + firstTable.rows.forEach((row) => { + const key = bucketColumns.map((c) => formatters[c.columnId].convert(row[c.columnId])).join(','); + if (!rowsByBucketColumns[key]) { + rowsByBucketColumns[key] = []; + } + rowsByBucketColumns[key].push(row); + }); + return rowsByBucketColumns; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 92136c557ad38..1848565114dea 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -9,7 +9,13 @@ import { Ast } from '@kbn/interpreter/common'; import { buildExpression } from '../../../../../src/plugins/expressions/public'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization } from './visualization'; -import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; +import { + Operation, + DataType, + FramePublicAPI, + TableSuggestionColumn, + VisualizationDimensionGroupConfig, +} from '../types'; function mockFrame(): FramePublicAPI { return { @@ -132,9 +138,9 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); expect(suggestions[0].state.columns).toEqual([ - { columnId: 'col1', width: 123 }, - { columnId: 'col2', hidden: true }, - { columnId: 'col3' }, + { columnId: 'col1', width: 123, isTransposed: false }, + { columnId: 'col2', hidden: true, isTransposed: false }, + { columnId: 'col3', isTransposed: false }, ]); expect(suggestions[0].state.sorting).toEqual({ columnId: 'col1', @@ -226,39 +232,45 @@ describe('Datatable Visualization', () => { }, frame, }).groups - ).toHaveLength(2); + ).toHaveLength(3); }); - it('allows only bucket operations one category', () => { + it('allows only bucket operations for splitting columns and rows', () => { const datasource = createMockDatasource('test'); const frame = mockFrame(); frame.datasourceLayers = { first: datasource.publicAPIMock }; - - const filterOperations = datatableVisualization.getConfiguration({ + const groups = datatableVisualization.getConfiguration({ layerId: 'first', state: { layerId: 'first', columns: [], }, frame, - }).groups[0].filterOperations; + }).groups; - const baseOperation: Operation = { - dataType: 'string', - isBucketed: true, - label: '', - }; - expect(filterOperations({ ...baseOperation })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual( - false - ); - expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual( - false - ); + function testGroup(group: VisualizationDimensionGroupConfig) { + const baseOperation: Operation = { + dataType: 'string', + isBucketed: true, + label: '', + }; + expect(group.filterOperations({ ...baseOperation })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual( + true + ); + expect( + group.filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false }) + ).toEqual(false); + expect( + group.filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false }) + ).toEqual(false); + } + + testGroup(groups[0]); + testGroup(groups[1]); }); it('allows only metric operations in one category', () => { @@ -273,7 +285,7 @@ describe('Datatable Visualization', () => { columns: [], }, frame, - }).groups[1].filterOperations; + }).groups[2].filterOperations; const baseOperation: Operation = { dataType: 'string', @@ -307,7 +319,7 @@ describe('Datatable Visualization', () => { columns: [{ columnId: 'b' }, { columnId: 'c' }], }, frame, - }).groups[1].accessors + }).groups[2].accessors ).toEqual([{ columnId: 'c' }, { columnId: 'b' }]); }); }); @@ -368,7 +380,7 @@ describe('Datatable Visualization', () => { }) ).toEqual({ layerId: 'layer1', - columns: [{ columnId: 'b' }, { columnId: 'c' }, { columnId: 'd' }], + columns: [{ columnId: 'b' }, { columnId: 'c' }, { columnId: 'd', isTransposed: false }], }); }); @@ -382,7 +394,7 @@ describe('Datatable Visualization', () => { }) ).toEqual({ layerId: 'layer1', - columns: [{ columnId: 'b' }, { columnId: 'c' }], + columns: [{ columnId: 'b', isTransposed: false }, { columnId: 'c' }], }); }); }); @@ -419,12 +431,16 @@ describe('Datatable Visualization', () => { columnId: ['c'], hidden: [], width: [], + isTransposed: [], + transposable: [true], alignment: [], }); expect(columnArgs[1].arguments).toEqual({ columnId: ['b'], hidden: [], width: [], + isTransposed: [], + transposable: [true], alignment: [], }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index b7ff23cdb6e35..4094ecee74e1c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -10,6 +10,7 @@ import { render } from 'react-dom'; import { Ast } from '@kbn/interpreter/common'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { DatatableColumn } from 'src/plugins/expressions/public'; import { SuggestionRequest, Visualization, @@ -23,6 +24,13 @@ export interface ColumnState { columnId: string; width?: number; hidden?: boolean; + isTransposed?: boolean; + // These flags are necessary to transpose columns and map them back later + // They are set automatically and are not user-editable + transposable?: boolean; + originalColumnId?: string; + originalName?: string; + bucketValues?: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>; alignment?: 'left' | 'right' | 'center'; } @@ -108,6 +116,11 @@ export const datatableVisualization: Visualization oldColumnSettings[column.columnId] = column; }); } + const lastTransposedColumnIndex = table.columns.findIndex((c) => + !oldColumnSettings[c.columnId] ? false : !oldColumnSettings[c.columnId]?.isTransposed + ); + const usesTransposing = state?.columns.some((c) => c.isTransposed); + const title = table.changeType === 'unchanged' ? i18n.translate('xpack.lens.datatable.suggestionLabel', { @@ -138,8 +151,9 @@ export const datatableVisualization: Visualization state: { ...(state || {}), layerId: table.layerId, - columns: table.columns.map((col) => ({ + columns: table.columns.map((col, columnIndex) => ({ ...(oldColumnSettings[col.columnId] || {}), + isTransposed: usesTransposing && columnIndex < lastTransposedColumnIndex, columnId: col.columnId, })), }, @@ -166,21 +180,55 @@ export const datatableVisualization: Visualization return { groups: [ { - groupId: 'columns', - groupLabel: i18n.translate('xpack.lens.datatable.breakdown', { - defaultMessage: 'Break down by', + groupId: 'rows', + groupLabel: i18n.translate('xpack.lens.datatable.breakdownRows', { + defaultMessage: 'Split rows', + }), + groupTooltip: i18n.translate('xpack.lens.datatable.breakdownRows.description', { + defaultMessage: + 'Split table rows by field. This is recommended for high cardinality breakdowns.', }), layerId: state.layerId, accessors: sortedColumns - .filter((c) => datasource!.getOperationForColumnId(c)?.isBucketed) + .filter( + (c) => + datasource!.getOperationForColumnId(c)?.isBucketed && + !state.columns.find((col) => col.columnId === c)?.isTransposed + ) .map((accessor) => ({ columnId: accessor, triggerIcon: columnMap[accessor].hidden ? 'invisible' : undefined, })), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, - dataTestSubj: 'lnsDatatable_column', + dataTestSubj: 'lnsDatatable_rows', + enableDimensionEditor: true, + hideGrouping: true, + nestingOrder: 1, + }, + { + groupId: 'columns', + groupLabel: i18n.translate('xpack.lens.datatable.breakdownColumns', { + defaultMessage: 'Split columns', + }), + groupTooltip: i18n.translate('xpack.lens.datatable.breakdownColumns.description', { + defaultMessage: + "Split metric columns by field. It's recommended to keep the number of columns low to avoid horizontal scrolling.", + }), + layerId: state.layerId, + accessors: sortedColumns + .filter( + (c) => + datasource!.getOperationForColumnId(c)?.isBucketed && + state.columns.find((col) => col.columnId === c)?.isTransposed + ) + .map((accessor) => ({ columnId: accessor })), + supportsMoreColumns: true, + filterOperations: (op) => op.isBucketed, + dataTestSubj: 'lnsDatatable_columns', enableDimensionEditor: true, + hideGrouping: true, + nestingOrder: 0, }, { groupId: 'metrics', @@ -204,13 +252,26 @@ export const datatableVisualization: Visualization }; }, - setDimension({ prevState, columnId }) { - if (prevState.columns.some((column) => column.columnId === columnId)) { - return prevState; + setDimension({ prevState, columnId, groupId, previousColumn }) { + if ( + prevState.columns.some( + (column) => + column.columnId === columnId || (previousColumn && column.columnId === previousColumn) + ) + ) { + return { + ...prevState, + columns: prevState.columns.map((column) => { + if (column.columnId === columnId || column.columnId === previousColumn) { + return { ...column, columnId, isTransposed: groupId === 'columns' }; + } + return column; + }), + }; } return { ...prevState, - columns: [...prevState.columns, { columnId }], + columns: [...prevState.columns, { columnId, isTransposed: groupId === 'columns' }], }; }, removeDimension({ prevState, columnId }) { @@ -268,6 +329,11 @@ export const datatableVisualization: Visualization columnId: [column.columnId], hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden], width: typeof column.width === 'undefined' ? [] : [column.width], + isTransposed: + typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed], + transposable: [ + !datasource!.getOperationForColumnId(column.columnId)?.isBucketed, + ], alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], }, }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index 04ab1318a12e0..8449727a9e79d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -40,6 +40,7 @@ export function DraggableDimensionButton({ layerIndex, columnId, group, + groups, onDrop, children, layerDatasourceDropProps, @@ -55,6 +56,7 @@ export function DraggableDimensionButton({ dropType?: DropType ) => void; group: VisualizationDimensionGroupConfig; + groups: VisualizationDimensionGroupConfig[]; label: string; children: React.ReactElement; layerDatasource: Datasource; @@ -71,6 +73,7 @@ export function DraggableDimensionButton({ columnId, filterOperations: group.filterOperations, groupId: group.groupId, + dimensionGroups: groups, }); const dropType = dropProps?.dropType; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index 664e24b989836..a6ccac1427fbf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -27,6 +27,7 @@ const getAdditionalClassesOnDroppable = (dropType?: string) => { export function EmptyDimensionButton({ group, + groups, layerDatasource, layerDatasourceDropProps, layerId, @@ -45,6 +46,8 @@ export function EmptyDimensionButton({ dropType?: DropType ) => void; group: VisualizationDimensionGroupConfig; + groups: VisualizationDimensionGroupConfig[]; + layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { @@ -63,6 +66,7 @@ export function EmptyDimensionButton({ columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, + dimensionGroups: groups, }); const dropType = dropProps?.dropType; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 1d75e873f9b18..14063aea02665 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -8,7 +8,14 @@ import './layer_panel.scss'; import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { + EuiPanel, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; @@ -151,6 +158,8 @@ export function LayerPanel( columnId, layerId: targetLayerId, filterOperations, + dimensionGroups: groups, + groupId, dropType, }); if (dropResult) { @@ -159,6 +168,7 @@ export function LayerPanel( groupId, layerId: targetLayerId, prevState: props.visualizationState, + previousColumn: typeof droppedItem.column === 'string' ? droppedItem.column : undefined, }); if (typeof dropResult === 'object') { @@ -254,7 +264,26 @@ export function LayerPanel( : 'lnsLayerPanel__row lnsLayerPanel__row--notSupportsMoreColumns' } fullWidth - label={
{group.groupLabel}
} + label={ +
+ {group.groupLabel} + {group.groupTooltip && ( + <> + {' '} + + + )} +
+ } labelType="legend" key={group.groupId} isInvalid={isMissing} @@ -281,6 +310,7 @@ export function LayerPanel( accessorIndex={accessorIndex} columnId={columnId} group={group} + groups={groups} groupIndex={groupIndex} key={columnId} layerDatasourceDropProps={layerDatasourceDropProps} @@ -325,6 +355,7 @@ export function LayerPanel( nativeProps={{ ...layerDatasourceConfigProps, columnId: accessorConfig.columnId, + groupId: group.groupId, filterOperations: group.filterOperations, }} /> @@ -338,6 +369,7 @@ export function LayerPanel( ); @@ -424,6 +434,8 @@ export function DimensionEditor(props: DimensionEditorProps) { indexPattern: currentIndexPattern, op: choice.operationType, field: currentIndexPattern.getFieldByName(choice.field), + visualizationGroups: dimensionGroups, + targetGroup: props.groupId, }) ); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 5eaa798f459e3..a6d2361be21d4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -197,6 +197,7 @@ describe('IndexPatternDimensionEditorPanel', () => { } as unknown) as DataPublicPluginStart, core: {} as CoreSetup, dimensionGroups: [], + groupId: 'a', }; jest.clearAllMocks(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 12df14f81cb67..82b6434e50aac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -7,12 +7,13 @@ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { onDrop, getDropProps } from './droppable'; +import { DraggingIdentifier } from '../../drag_drop'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { OperationMetadata, DropType } from '../../types'; -import { IndexPatternColumn } from '../operations'; +import { IndexPatternColumn, MedianIndexPatternColumn } from '../operations'; import { getFieldByNameFactory } from '../pure_helpers'; const fields = [ @@ -48,6 +49,22 @@ const fields = [ searchable: true, exists: true, }, + { + name: 'src', + displayName: 'src', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, documentField, ]; @@ -144,6 +161,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columnId: 'col1', layerId: 'first', uniqueLabel: 'stuff', + groupId: 'group1', filterOperations: () => true, storage: {} as IStorageWrapper, uiSettings: {} as IUiSettingsClient, @@ -572,36 +590,458 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); - it('copies a dimension if dropType is duplicate_in_group, respecting bucket metric order', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, + describe('dimension group aware ordering and copying', () => { + let dragging: DraggingIdentifier; + let testState: IndexPatternPrivateState; + beforeEach(() => { + dragging = { + columnId: 'col2', + groupId: 'b', + layerId: 'first', + id: 'col2', + humanData: { + label: '', + }, + }; + testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, - col2: { - label: 'Top values of src', - dataType: 'string', - isBucketed: true, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Top values of dest', + dataType: 'string', + isBucketed: true, - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'dest', + }, + col4: { + label: 'Median of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'median', + sourceField: 'bytes', }, - sourceField: 'src', }, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, + }; + }); + const dimensionGroups = [ + { + accessors: [], + groupId: 'a', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: () => false, + }, + { + accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], + groupId: 'b', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: () => false, + }, + { + accessors: [{ columnId: 'col4' }], + groupId: 'c', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: () => false, + }, + ]; - // Private - operationType: 'count', - sourceField: 'Records', + it('respects groups on moving operations from one group to another', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + columnId: 'newCol', + droppedItem: dragging, + state: testState, + groupId: 'a', + dimensionGroups, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations from one group to another with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + const draggingCol3 = { + columnId: 'col3', + groupId: 'b', + layerId: 'first', + id: 'col3', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'col1', + droppedItem: draggingCol3, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves newly created dimension to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col1 into newCol in group b + const draggingCol1 = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'move_compatible', + droppedItem: draggingCol1, + state: testState, + groupId: 'b', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col2', 'col3', 'newCol', 'col4'], + columns: { + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('appends the dropped column in the right place when a field is dropped', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + const draggingBytesField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'foo', + id: 'bar', + humanData: { + label: '', + }, + }; + + onDrop({ + ...defaultProps, + droppedItem: draggingBytesField, + columnId: 'newCol', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + dimensionGroups, + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('appends the dropped column in the right place respecting custom nestingOrder', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + const draggingBytesField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'foo', + id: 'bar', + humanData: { + label: '', + }, + }; + + onDrop({ + ...defaultProps, + droppedItem: draggingBytesField, + columnId: 'newCol', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + dimensionGroups: [ + // a and b are ordered in reverse visually, but nesting order keeps them in place for column order + { ...dimensionGroups[1], nestingOrder: 1 }, + { ...dimensionGroups[0], nestingOrder: 0 }, + { ...dimensionGroups[2] }, + ], + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('copies column to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // copying col1 within group a + const draggingCol1 = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'duplicate_in_group', + droppedItem: draggingCol1, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + const draggingCol4 = { + columnId: 'col4', + groupId: 'c', + layerId: 'first', + id: 'col4', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'move_incompatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + }); + + it('if dnd is reorder, it correctly reorders columns', () => { + const testState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + col2: { + label: 'Top values of bar', + dataType: 'number', + isBucketed: true, + operationType: 'terms', + sourceField: 'bar', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }, + col3: { + operationType: 'avg', + sourceField: 'memory', + label: 'average of memory', + dataType: 'number', + isBucketed: false, + }, + }, }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index a7d4774d8aa3d..e846db718f1d3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -17,6 +17,8 @@ import { insertOrReplaceColumn, deleteColumn, getOperationTypesForField, + getColumnOrder, + reorderByGroups, getOperationDisplay, } from '../operations'; import { mergeLayer } from '../state_helpers'; @@ -191,7 +193,7 @@ function onReorderDrop({ } function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { - const { columnId, setState, state, layerId, droppedItem } = props; + const { columnId, setState, state, layerId, droppedItem, dimensionGroups, groupId } = props; const layer = state.layers[layerId]; const op = { ...layer.columns[droppedItem.columnId] }; @@ -225,6 +227,8 @@ function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { const layer = state.layers[layerId]; @@ -258,19 +264,29 @@ function onSameGroupDuplicateDrop({ const newColumnOrder = [...layer.columnOrder]; // put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array - // TODO this logic does not take into account groups - we probably need to pass the current - // group config to this position to place the column right + // then reorder based on dimension groups if necessary const insertionIndex = op.isBucketed ? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed) : newColumnOrder.length; newColumnOrder.splice(insertionIndex, 0, columnId); + + const newLayer = { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }; + + const updatedColumnOrder = getColumnOrder(newLayer); + + reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); + // Time to replace setState( mergeLayer({ state, layerId, newLayer: { - columnOrder: newColumnOrder, + columnOrder: updatedColumnOrder, columns: newColumns, }, }) @@ -284,6 +300,8 @@ function onMoveDropToCompatibleGroup({ state, layerId, droppedItem, + dimensionGroups, + groupId, }: DropHandlerProps) { const layer = state.layers[layerId]; const op = { ...layer.columns[droppedItem.columnId] }; @@ -296,18 +314,31 @@ function onMoveDropToCompatibleGroup({ const newIndex = newColumnOrder.findIndex((c) => c === columnId); if (newIndex === -1) { - newColumnOrder[oldIndex] = columnId; - } else { + // for newly created columns, remove the old entry and add the last one to the end newColumnOrder.splice(oldIndex, 1); + newColumnOrder.push(columnId); + } else { + // for drop to replace, reuse the same index + newColumnOrder[oldIndex] = columnId; } + const newLayer = { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }; + + const updatedColumnOrder = getColumnOrder(newLayer); + + reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); // Time to replace setState( mergeLayer({ state, layerId, + newLayer: { - columnOrder: newColumnOrder, + columnOrder: updatedColumnOrder, columns: newColumns, }, }) @@ -316,7 +347,7 @@ function onMoveDropToCompatibleGroup({ } function onFieldDrop(props: DropHandlerProps) { - const { columnId, setState, state, layerId, droppedItem } = props; + const { columnId, setState, state, layerId, droppedItem, groupId, dimensionGroups } = props; const operationsForNewField = getOperationTypesForField( droppedItem.field, @@ -343,6 +374,8 @@ function onFieldDrop(props: DropHandlerProps) { indexPattern: currentIndexPattern, op: fieldIsCompatibleWithCurrent ? selectedColumn.operationType : operationsForNewField[0], field: droppedItem.field, + visualizationGroups: dimensionGroups, + targetGroup: groupId, }); trackUiEvent('drop_onto_dimension'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index dddebfbff1466..9ad6a2d20a4c2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -50,6 +50,7 @@ describe('reference editor', () => { savedObjectsClient: {} as SavedObjectsClientContract, http: {} as HttpSetup, data: {} as DataPublicPluginStart, + dimensionGroups: [], }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index 87be81f66e8e7..353bba9652eff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -35,6 +35,7 @@ import { FieldSelect } from './field_select'; import { hasField } from '../utils'; import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { VisualizationDimensionGroupConfig } from '../../types'; const operationPanels = getOperationDisplay(); @@ -48,6 +49,7 @@ export interface ReferenceEditorProps { existingFields: IndexPatternPrivateState['existingFields']; dateRange: DateRange; labelAppend?: EuiFormRowProps['labelAppend']; + dimensionGroups: VisualizationDimensionGroupConfig[]; // Services uiSettings: IUiSettingsClient; @@ -68,6 +70,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { selectionStyle, dateRange, labelAppend, + dimensionGroups, ...services } = props; @@ -168,6 +171,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { op: operationType, indexPattern: currentIndexPattern, field: currentIndexPattern.getFieldByName(column.sourceField), + visualizationGroups: dimensionGroups, }) ); } else { @@ -185,6 +189,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { op: operationType, indexPattern: currentIndexPattern, field: possibleField, + visualizationGroups: dimensionGroups, }) ); } @@ -257,7 +262,11 @@ export function ReferenceEditor(props: ReferenceEditorProps) { onChange={(choices) => { if (choices.length === 0) { updateLayer( - deleteColumn({ layer, columnId, indexPattern: currentIndexPattern }) + deleteColumn({ + layer, + columnId, + indexPattern: currentIndexPattern, + }) ); return; } @@ -298,7 +307,13 @@ export function ReferenceEditor(props: ReferenceEditorProps) { incompleteOperation={incompleteOperation} markAllFieldsCompatible={selectionStyle === 'field'} onDeleteColumn={() => { - updateLayer(deleteColumn({ layer, columnId, indexPattern: currentIndexPattern })); + updateLayer( + deleteColumn({ + layer, + columnId, + indexPattern: currentIndexPattern, + }) + ); }} onChoose={(choice) => { updateLayer( @@ -308,6 +323,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { indexPattern: currentIndexPattern, op: choice.operationType, field: currentIndexPattern.getFieldByName(choice.field), + visualizationGroups: dimensionGroups, }) ); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index cd7cfc6e8a1b2..64da5e4fb9f74 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -171,7 +171,11 @@ export function getIndexPatternDatasource({ return mergeLayer({ state: prevState, layerId, - newLayer: deleteColumn({ layer: prevState.layers[layerId], columnId, indexPattern }), + newLayer: deleteColumn({ + layer: prevState.layers[layerId], + columnId, + indexPattern, + }), }); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index e62764cbfef8d..bde07c182555e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -182,6 +182,7 @@ function getExistingLayerSuggestionsForField( field, op: usableAsBucketOperation, columnId: previousDate, + visualizationGroups: [], }), layerId, changeType: 'initial', @@ -197,6 +198,7 @@ function getExistingLayerSuggestionsForField( field, op: usableAsBucketOperation, columnId: generateId(), + visualizationGroups: [], }), layerId, changeType: 'extended', @@ -214,6 +216,7 @@ function getExistingLayerSuggestionsForField( field, columnId: generateId(), op: metricOperation.type, + visualizationGroups: [], }); if (layerWithNewMetric) { suggestions.push( @@ -235,6 +238,7 @@ function getExistingLayerSuggestionsForField( field, columnId: metrics[0], op: metricOperation.type, + visualizationGroups: [], }); if (layerWithReplacedMetric) { suggestions.push( @@ -302,10 +306,12 @@ function createNewLayerWithBucketAggregation( columnId: generateId(), field: documentField, indexPattern, + visualizationGroups: [], }), columnId: generateId(), field, indexPattern, + visualizationGroups: [], }); } @@ -327,10 +333,12 @@ function createNewLayerWithMetricAggregation( columnId: generateId(), field, indexPattern, + visualizationGroups: [], }), columnId: generateId(), field: dateField, indexPattern, + visualizationGroups: [], }); } @@ -483,6 +491,7 @@ function createMetricSuggestion( op: operation.type, field: operation.type === 'count' ? documentField : field, indexPattern, + visualizationGroups: [], }), }); } @@ -525,6 +534,7 @@ function createAlternativeMetricSuggestions( field, columnId, op: possibleOperations[0].type, + visualizationGroups: [], }); if (layerWithNewMetric) { suggestions.push( @@ -558,6 +568,7 @@ function createSuggestionWithDefaultDateHistogram( field: timeField, op: 'date_histogram', columnId: generateId(), + visualizationGroups: [], }), label: i18n.translate('xpack.lens.indexpattern.suggestions.overTimeLabel', { defaultMessage: 'Over time', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 1961a4f957d81..4f915160a52a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -104,6 +104,7 @@ describe('state_helpers', () => { indexPattern, op: 'missing' as OperationType, columnId: 'none', + visualizationGroups: [], }); }).toThrow(); }); @@ -130,6 +131,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'filters', + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col2', 'col1'] })); }); @@ -157,6 +159,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col2', 'col1'] })); }); @@ -187,6 +190,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); @@ -225,6 +229,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); @@ -263,6 +268,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'filters', + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); @@ -275,6 +281,7 @@ describe('state_helpers', () => { indexPattern, op: 'terms', field: indexPattern.fields[0], + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -310,6 +317,7 @@ describe('state_helpers', () => { indexPattern, op: 'terms', field: indexPattern.fields[2], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col2', 'col1'] })); }); @@ -339,6 +347,7 @@ describe('state_helpers', () => { indexPattern, op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); @@ -374,6 +383,7 @@ describe('state_helpers', () => { indexPattern, op: 'sum', field: indexPattern.fields[2], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); @@ -395,6 +405,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'testReference' as OperationType, + visualizationGroups: [], }); }).toThrow(); }); @@ -406,6 +417,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'testReference' as OperationType, + visualizationGroups: [], }); expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( @@ -470,6 +482,7 @@ describe('state_helpers', () => { columnId: 'ref1', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -492,6 +505,7 @@ describe('state_helpers', () => { op: 'count', field: documentField, columnId: 'none', + visualizationGroups: [], }); }).toThrow(); }); @@ -503,6 +517,7 @@ describe('state_helpers', () => { indexPattern, op: 'missing' as OperationType, columnId: 'none', + visualizationGroups: [], }); }).toThrow(); }); @@ -539,6 +554,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'date_histogram', field: indexPattern.fields[0], // date + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -572,6 +588,7 @@ describe('state_helpers', () => { indexPattern, op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }); }).toThrow(); }); @@ -600,6 +617,7 @@ describe('state_helpers', () => { columnId: 'col1', indexPattern, op: 'terms', + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -636,6 +654,7 @@ describe('state_helpers', () => { indexPattern, op: 'date_histogram', field: indexPattern.fields[1], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -671,6 +690,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col1', op: 'filters', + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -706,6 +726,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -740,6 +761,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'date_histogram', field: indexPattern.fields[1], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -775,6 +797,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'terms', field: indexPattern.fields[0], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -819,6 +842,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'avg', field: indexPattern.fields[2], // bytes field + visualizationGroups: [], }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( @@ -876,6 +900,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'willBeReference', op: 'cumulative_sum', + visualizationGroups: [], }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( @@ -925,6 +950,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col1', op: 'testReference' as OperationType, + visualizationGroups: [], }); expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( @@ -1333,6 +1359,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'filters', + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -1376,6 +1403,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -1414,6 +1442,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'ref', op: 'sum', + visualizationGroups: [], }); expect(result.columnOrder).toEqual(['ref']); @@ -1466,6 +1495,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 15acdcd52860a..3a67e8e464323 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -6,7 +6,7 @@ */ import _, { partition } from 'lodash'; -import type { OperationMetadata } from '../../types'; +import type { OperationMetadata, VisualizationDimensionGroupConfig } from '../../types'; import { operationDefinitionMap, operationDefinitions, @@ -25,6 +25,8 @@ interface ColumnChange { columnId: string; indexPattern: IndexPattern; field?: IndexPatternField; + visualizationGroups: VisualizationDimensionGroupConfig[]; + targetGroup?: string; } export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { @@ -42,6 +44,8 @@ export function insertNewColumn({ columnId, field, indexPattern, + visualizationGroups, + targetGroup, }: ColumnChange): IndexPatternLayer { const operationDefinition = operationDefinitionMap[op]; @@ -63,7 +67,13 @@ export function insertNewColumn({ const isBucketed = Boolean(possibleOperation.isBucketed); const addOperationFn = isBucketed ? addBucket : addMetric; return updateDefaultLabels( - addOperationFn(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId), + addOperationFn( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer }), + columnId, + visualizationGroups, + targetGroup + ), indexPattern ); } @@ -97,6 +107,8 @@ export function insertNewColumn({ columnId: newId, op: def.type, indexPattern, + visualizationGroups, + targetGroup, }); } else if (validFields.length === 1) { // Recursively update the layer for each new reference @@ -106,6 +118,8 @@ export function insertNewColumn({ op: def.type, indexPattern, field: validFields[0], + visualizationGroups, + targetGroup, }); } else { tempLayer = { @@ -133,7 +147,9 @@ export function insertNewColumn({ addOperationFn( tempLayer, operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, referenceIds }), - columnId + columnId, + visualizationGroups, + targetGroup ), indexPattern ); @@ -155,7 +171,9 @@ export function insertNewColumn({ addBucket( layer, operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), - columnId + columnId, + visualizationGroups, + targetGroup ), indexPattern ); @@ -196,7 +214,9 @@ export function insertNewColumn({ addOperationFn( layer, operationDefinition.buildColumn({ ...baseOptions, layer, field }), - columnId + columnId, + visualizationGroups, + targetGroup ), indexPattern ); @@ -208,6 +228,7 @@ export function replaceColumn({ indexPattern, op, field, + visualizationGroups, }: ColumnChange): IndexPatternLayer { const previousColumn = layer.columns[columnId]; if (!previousColumn) { @@ -240,6 +261,7 @@ export function replaceColumn({ previousColumn, op, indexPattern, + visualizationGroups, }); } @@ -297,7 +319,11 @@ export function replaceColumn({ // This logic comes after the transitions because they need to look at previous columns if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + tempLayer = deleteColumn({ + layer: tempLayer, + columnId: id, + indexPattern, + }); }); } @@ -385,6 +411,7 @@ export function canTransition({ field, indexPattern, filterOperations, + visualizationGroups, }: ColumnChange & { filterOperations: (meta: OperationMetadata) => boolean; }): boolean { @@ -398,7 +425,14 @@ export function canTransition({ } try { - const newLayer = replaceColumn({ layer, columnId, op, field, indexPattern }); + const newLayer = replaceColumn({ + layer, + columnId, + op, + field, + indexPattern, + visualizationGroups, + }); const newDefinition = operationDefinitionMap[op]; const newColumn = newLayer.columns[columnId]; return ( @@ -437,12 +471,14 @@ function applyReferenceTransition({ previousColumn, op, indexPattern, + visualizationGroups, }: { layer: IndexPatternLayer; columnId: string; previousColumn: IndexPatternColumn; op: OperationType; indexPattern: IndexPattern; + visualizationGroups: VisualizationDimensionGroupConfig[]; }): IndexPatternLayer { const operationDefinition = operationDefinitionMap[op]; @@ -499,6 +535,7 @@ function applyReferenceTransition({ columnId: newId, op: validOperations[0].type, indexPattern, + visualizationGroups, }); return newId; } @@ -536,6 +573,7 @@ function applyReferenceTransition({ op: defWithField[0].type, indexPattern, field: indexPattern.getFieldByName(previousColumn.sourceField), + visualizationGroups, }); return newId; } else if (defIgnoringfield.length === 1) { @@ -578,6 +616,7 @@ function applyReferenceTransition({ op: defWithField[0].type, indexPattern, field: previousField, + visualizationGroups, }); return newId; } @@ -633,7 +672,9 @@ function copyCustomLabel(newColumn: IndexPatternColumn, previousColumn: IndexPat function addBucket( layer: IndexPatternLayer, column: IndexPatternColumn, - addedColumnId: string + addedColumnId: string, + visualizationGroups: VisualizationDimensionGroupConfig[], + targetGroup?: string ): IndexPatternLayer { const [buckets, metrics, references] = getExistingColumnGroups(layer); @@ -656,6 +697,7 @@ function addBucket( // they already had, with an extra level of detail. updatedColumnOrder = [...buckets, addedColumnId, ...metrics, ...references]; } + reorderByGroups(visualizationGroups, targetGroup, updatedColumnOrder, addedColumnId); const tempLayer = { ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column }, @@ -664,6 +706,43 @@ function addBucket( return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } +export function reorderByGroups( + visualizationGroups: VisualizationDimensionGroupConfig[], + targetGroup: string | undefined, + updatedColumnOrder: string[], + addedColumnId: string +) { + const hidesColumnGrouping = + targetGroup && visualizationGroups.find((group) => group.groupId === targetGroup)?.hideGrouping; + + // if column grouping is disabled, keep bucket aggregations in the same order as the groups + // if grouping is known + if (hidesColumnGrouping) { + const orderedVisualizationGroups = [...visualizationGroups]; + orderedVisualizationGroups.sort((group1, group2) => { + if (typeof group1.nestingOrder === undefined) { + return -1; + } + if (typeof group2.nestingOrder === undefined) { + return 1; + } + return group1.nestingOrder! - group2.nestingOrder!; + }); + const columnGroupIndex: Record = {}; + updatedColumnOrder.forEach((columnId) => { + columnGroupIndex[columnId] = orderedVisualizationGroups.findIndex( + (group) => + (columnId === addedColumnId && group.groupId === targetGroup) || + group.accessors.some((acc) => acc.columnId === columnId) + ); + }); + + updatedColumnOrder.sort((a, b) => { + return columnGroupIndex[a] - columnGroupIndex[b]; + }); + } +} + function addMetric( layer: IndexPatternLayer, column: IndexPatternColumn, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index e42ca36e5cfc3..3b47792af4254 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -264,6 +264,7 @@ interface SharedDimensionProps { export type DatasourceDimensionProps = SharedDimensionProps & { layerId: string; columnId: string; + groupId: string; onRemove?: (accessor: string) => void; state: T; activeData?: Record; @@ -308,9 +309,11 @@ export function isDraggedOperation( export type DatasourceDimensionDropProps = SharedDimensionProps & { layerId: string; + groupId: string; columnId: string; state: T; setState: StateSetter; + dimensionGroups: VisualizationDimensionGroupConfig[]; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { @@ -388,6 +391,7 @@ export interface AccessorConfig { export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; + groupTooltip?: string; /** ID is passed back to visualization. For example, `x` */ groupId: string; @@ -402,6 +406,10 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { * will render the extra tab for the dimension editor */ enableDimensionEditor?: boolean; + // if the visual order of dimension groups diverges from the intended nesting order, this property specifies the position of + // this dimension group in the hierarchy. If not specified, the position of the dimension in the array is used. specified nesting + // orders are always higher in the hierarchy than non-specified ones. + nestingOrder?: number; }; interface VisualizationDimensionChangeProps { @@ -592,7 +600,9 @@ export interface Visualization { * The frame is telling the visualization to update or set a dimension based on user interaction * groupId is coming from the groupId provided in getConfiguration */ - setDimension: (props: VisualizationDimensionChangeProps & { groupId: string }) => T; + setDimension: ( + props: VisualizationDimensionChangeProps & { groupId: string; previousColumn?: string } + ) => T; /** * The frame is telling the visualization to remove a dimension. The visualization needs to * look at its internal state to determine which dimension is being affected. diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index c5e7b0af9654f..0bf5c139e2403 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -648,6 +648,14 @@ export function XYChart({ layersAlreadyFormatted ); + const isStacked = seriesType.includes('stacked'); + const isPercentage = seriesType.includes('percentage'); + const isBarChart = seriesType.includes('bar'); + const enableHistogramMode = + isHistogram && + (isStacked || !splitAccessor) && + (isStacked || !isBarChart || !chartHasMoreThanOneBarSeries); + // For date histogram chart type, we're getting the rows that represent intervals without data. // To not display them in the legend, they need to be filtered out. const rows = tableConverted.rows.filter( @@ -674,7 +682,7 @@ export function XYChart({ const seriesProps: SeriesSpec = { splitSeriesAccessors: splitAccessor ? [splitAccessor] : [], - stackAccessors: seriesType.includes('stacked') ? [xAccessor as string] : [], + stackAccessors: isStacked ? [xAccessor as string] : [], id: `${splitAccessor}-${accessor}`, xAccessor: xAccessor || 'unifiedX', yAccessors: [accessor], @@ -710,13 +718,8 @@ export function XYChart({ ); }, groupId: yAxis?.groupId, - enableHistogramMode: - isHistogram && - (seriesType.includes('stacked') || !splitAccessor) && - (seriesType.includes('stacked') || - !seriesType.includes('bar') || - !chartHasMoreThanOneBarSeries), - stackMode: seriesType.includes('percentage') ? StackMode.Percentage : undefined, + enableHistogramMode, + stackMode: isPercentage ? StackMode.Percentage : undefined, timeZone, areaSeriesStyle: { point: { @@ -797,7 +800,11 @@ export function XYChart({ case 'area_stacked': case 'area_percentage_stacked': return ( - + ); case 'area': return ( diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 06e72b0070dfe..23da48b35a9d4 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -44,6 +44,7 @@ export { namespaceType, ExceptionListType, Type, + osType, osTypeArray, OsTypeArray, } from './schemas'; diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index cfceb3e8b422e..f1e0ac25aa127 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -131,15 +131,15 @@ export enum ES_SPATIAL_RELATIONS { WITHIN = 'WITHIN', } -export const GEO_JSON_TYPE = { - POINT: 'Point', - MULTI_POINT: 'MultiPoint', - LINE_STRING: 'LineString', - MULTI_LINE_STRING: 'MultiLineString', - POLYGON: 'Polygon', - MULTI_POLYGON: 'MultiPolygon', - GEOMETRY_COLLECTION: 'GeometryCollection', -}; +export enum GEO_JSON_TYPE { + POINT = 'Point', + MULTI_POINT = 'MultiPoint', + LINE_STRING = 'LineString', + MULTI_LINE_STRING = 'MultiLineString', + POLYGON = 'Polygon', + MULTI_POLYGON = 'MultiPolygon', + GEOMETRY_COLLECTION = 'GeometryCollection', +} export const POLYGON_COORDINATES_EXTERIOR_INDEX = 0; export const LON_INDEX = 0; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts deleted file mode 100644 index 2a3741146d454..0000000000000 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts +++ /dev/null @@ -1,53 +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 { FeatureCollection, GeoJsonProperties, Polygon } from 'geojson'; -import { MapExtent } from '../descriptor_types'; -import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; - -export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; - -export function turfBboxToBounds(turfBbox: unknown): MapExtent; - -export function clampToLatBounds(lat: number): number; - -export function clampToLonBounds(lon: number): number; - -export function hitsToGeoJson( - hits: Array>, - flattenHit: (elasticSearchHit: Record) => GeoJsonProperties, - geoFieldName: string, - geoFieldType: ES_GEO_FIELD_TYPE, - epochMillisFields: string[] -): FeatureCollection; - -export interface ESBBox { - top_left: number[]; - bottom_right: number[]; -} - -export interface ESGeoBoundingBoxFilter { - geo_bounding_box: { - [geoFieldName: string]: ESBBox; - }; -} - -export interface ESPolygonFilter { - geo_shape: { - [geoFieldName: string]: { - shape: Polygon; - relation: ES_SPATIAL_RELATIONS.INTERSECTS; - }; - }; -} - -export function createExtentFilter( - mapExtent: MapExtent, - geoFieldName: string -): ESPolygonFilter | ESGeoBoundingBoxFilter; - -export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBox; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index 9983bb9b84588..22b8a86158a74 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -397,12 +397,10 @@ describe('createExtentFilter', () => { minLon: -89, }; const filter = createExtentFilter(mapExtent, geoFieldName); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [-89, 39], - bottom_right: [-83, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], }, }); }); @@ -415,12 +413,10 @@ describe('createExtentFilter', () => { minLon: -190, }; const filter = createExtentFilter(mapExtent, geoFieldName); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [-180, 89], - bottom_right: [180, -89], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 89], + bottom_right: [180, -89], }, }); }); @@ -436,12 +432,10 @@ describe('createExtentFilter', () => { const leftLon = filter.geo_bounding_box.location.top_left[0]; const rightLon = filter.geo_bounding_box.location.bottom_right[0]; expect(leftLon).toBeGreaterThan(rightLon); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [100, 39], - bottom_right: [-160, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [100, 39], + bottom_right: [-160, 35], }, }); }); @@ -457,12 +451,10 @@ describe('createExtentFilter', () => { const leftLon = filter.geo_bounding_box.location.top_left[0]; const rightLon = filter.geo_bounding_box.location.bottom_right[0]; expect(leftLon).toBeGreaterThan(rightLon); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [160, 39], - bottom_right: [-100, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [160, 39], + bottom_right: [-100, 35], }, }); }); @@ -475,12 +467,10 @@ describe('createExtentFilter', () => { minLon: -191, }; const filter = createExtentFilter(mapExtent, geoFieldName); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [-180, 39], - bottom_right: [180, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 39], + bottom_right: [180, 35], }, }); }); diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts similarity index 70% rename from x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js rename to x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts index 47de8850c0e96..f529f187c690c 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts @@ -7,7 +7,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +// @ts-expect-error import { parse } from 'wellknown'; +// @ts-expect-error +import turfCircle from '@turf/circle'; +import { Feature, FeatureCollection, Geometry, Polygon, Point, Position } from 'geojson'; +import { BBox } from '@turf/helpers'; import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, @@ -18,14 +23,54 @@ import { LAT_INDEX, } from '../constants'; import { getEsSpatialRelationLabel } from '../i18n_getters'; -import { FILTERS } from '../../../../../src/plugins/data/common'; -import turfCircle from '@turf/circle'; +import { Filter, FILTERS } from '../../../../../src/plugins/data/common'; +import { MapExtent } from '../descriptor_types'; const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; -function ensureGeoField(type) { +type Coordinates = Position | Position[] | Position[][] | Position[][][]; + +// Elasticsearch stores more then just GeoJSON. +// 1) geometry.type as lower case string +// 2) circle and envelope types +interface ESGeometry { + type: string; + coordinates: Coordinates; +} + +export interface ESBBox { + top_left: number[]; + bottom_right: number[]; +} + +interface GeoShapeQueryBody { + shape?: Polygon; + relation?: ES_SPATIAL_RELATIONS; + indexed_shape?: PreIndexedShape; +} + +export type GeoFilter = Filter & { + geo_bounding_box?: { + [geoFieldName: string]: ESBBox; + }; + geo_distance?: { + distance: string; + [geoFieldName: string]: Position | { lat: number; lon: number } | string; + }; + geo_shape?: { + [geoFieldName: string]: GeoShapeQueryBody; + }; +}; + +export interface PreIndexedShape { + index: string; + id: string | number; + path: string; +} + +function ensureGeoField(type: string) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; - if (!expectedTypes.includes(type)) { + if (!expectedTypes.includes(type as ES_GEO_FIELD_TYPE)) { const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage', { @@ -41,8 +86,8 @@ function ensureGeoField(type) { } } -function ensureGeometryType(type, expectedTypes) { - if (!expectedTypes.includes(type)) { +function ensureGeometryType(type: string, expectedTypes: GEO_JSON_TYPE[]) { + if (!expectedTypes.includes(type as GEO_JSON_TYPE)) { const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage', { @@ -68,36 +113,48 @@ function ensureGeometryType(type, expectedTypes) { * @param {string} geoFieldType Geometry field type ["geo_point", "geo_shape"] * @returns {number} */ -export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epochMillisFields) { - const features = []; - const tmpGeometriesAccumulator = []; +export function hitsToGeoJson( + hits: Array>, + flattenHit: (elasticSearchHit: Record) => Record, + geoFieldName: string, + geoFieldType: ES_GEO_FIELD_TYPE, + epochMillisFields: string[] +): FeatureCollection { + const features: Feature[] = []; + const tmpGeometriesAccumulator: Geometry[] = []; for (let i = 0; i < hits.length; i++) { const properties = flattenHit(hits[i]); - tmpGeometriesAccumulator.length = 0; //truncate accumulator + tmpGeometriesAccumulator.length = 0; // truncate accumulator ensureGeoField(geoFieldType); if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - geoPointToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); + geoPointToGeometry( + properties[geoFieldName] as string | string[] | undefined, + tmpGeometriesAccumulator + ); } else { - geoShapeToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); + geoShapeToGeometry( + properties[geoFieldName] as string | string[] | ESGeometry | ESGeometry[] | undefined, + tmpGeometriesAccumulator + ); } // There is a bug in Elasticsearch API where epoch_millis are returned as a string instead of a number // https://github.com/elastic/elasticsearch/issues/50622 // Convert these field values to integers. - for (let i = 0; i < epochMillisFields.length; i++) { - const fieldName = epochMillisFields[i]; + for (let k = 0; k < epochMillisFields.length; k++) { + const fieldName = epochMillisFields[k]; if (typeof properties[fieldName] === 'string') { - properties[fieldName] = parseInt(properties[fieldName]); + properties[fieldName] = parseInt(properties[fieldName] as string, 10); } } // don't include geometry field value in properties delete properties[geoFieldName]; - //create new geojson Feature for every individual geojson geometry. + // create new geojson Feature for every individual geojson geometry. for (let j = 0; j < tmpGeometriesAccumulator.length; j++) { features.push({ type: 'Feature', @@ -112,7 +169,7 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epoc return { type: 'FeatureCollection', - features: features, + features, }; } @@ -120,7 +177,10 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epoc // Either // 1) Array of latLon strings // 2) latLon string -export function geoPointToGeometry(value, accumulator) { +export function geoPointToGeometry( + value: string[] | string | undefined, + accumulator: Geometry[] +): void { if (!value) { return; } @@ -138,10 +198,10 @@ export function geoPointToGeometry(value, accumulator) { accumulator.push({ type: GEO_JSON_TYPE.POINT, coordinates: [lon, lat], - }); + } as Point); } -export function convertESShapeToGeojsonGeometry(value) { +export function convertESShapeToGeojsonGeometry(value: ESGeometry): Geometry { const geoJson = { type: value.type, coordinates: value.coordinates, @@ -183,12 +243,13 @@ export function convertESShapeToGeojsonGeometry(value) { ); throw new Error(invalidGeometrycollectionError); case 'envelope': + const envelopeCoords = geoJson.coordinates as Position[]; // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope const polygon = formatEnvelopeAsPolygon({ - minLon: geoJson.coordinates[0][0], - maxLon: geoJson.coordinates[1][0], - minLat: geoJson.coordinates[1][1], - maxLat: geoJson.coordinates[0][1], + minLon: envelopeCoords[0][0], + maxLon: envelopeCoords[1][0], + minLat: envelopeCoords[1][1], + maxLat: envelopeCoords[0][1], }); geoJson.type = polygon.type; geoJson.coordinates = polygon.coordinates; @@ -205,10 +266,10 @@ export function convertESShapeToGeojsonGeometry(value) { ); throw new Error(errorMessage); } - return geoJson; + return (geoJson as unknown) as Geometry; } -function convertWKTStringToGeojson(value) { +function convertWKTStringToGeojson(value: string): Geometry { try { return parse(value); } catch (e) { @@ -222,7 +283,10 @@ function convertWKTStringToGeojson(value) { } } -export function geoShapeToGeometry(value, accumulator) { +export function geoShapeToGeometry( + value: string | ESGeometry | string[] | ESGeometry[] | undefined, + accumulator: Geometry[] +): void { if (!value) { return; } @@ -243,8 +307,9 @@ export function geoShapeToGeometry(value, accumulator) { value.type === GEO_JSON_TYPE.GEOMETRY_COLLECTION || value.type === 'geometrycollection' ) { - for (let i = 0; i < value.geometries.length; i++) { - geoShapeToGeometry(value.geometries[i], accumulator); + const geometryCollection = (value as unknown) as { geometries: ESGeometry[] }; + for (let i = 0; i < geometryCollection.geometries.length; i++) { + geoShapeToGeometry(geometryCollection.geometries[i], accumulator); } } else { const geoJson = convertESShapeToGeojsonGeometry(value); @@ -252,7 +317,7 @@ export function geoShapeToGeometry(value, accumulator) { } } -export function makeESBbox({ maxLat, maxLon, minLat, minLon }) { +export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBox { const bottom = clampToLatBounds(minLat); const top = clampToLatBounds(maxLat); let esBbox; @@ -280,11 +345,16 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }) { return esBbox; } -export function createExtentFilter(mapExtent, geoFieldName) { - const boundingBox = makeESBbox(mapExtent); +export function createExtentFilter(mapExtent: MapExtent, geoFieldName: string): GeoFilter { return { geo_bounding_box: { - [geoFieldName]: boundingBox, + [geoFieldName]: makeESBbox(mapExtent), + }, + meta: { + alias: null, + disabled: false, + negate: false, + key: geoFieldName, }, }; } @@ -297,6 +367,14 @@ export function createSpatialFilterWithGeometry({ geoFieldName, geoFieldType, relation = ES_SPATIAL_RELATIONS.INTERSECTS, +}: { + preIndexedShape?: PreIndexedShape; + geometry: Polygon; + geometryLabel: string; + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + relation: ES_SPATIAL_RELATIONS; }) { ensureGeoField(geoFieldType); @@ -315,7 +393,7 @@ export function createSpatialFilterWithGeometry({ alias: `${geoFieldName} ${relationLabel} ${geometryLabel}`, }; - const shapeQuery = { + const shapeQuery: GeoShapeQueryBody = { // geo_shape query with geo_point field only supports intersects relation relation: isGeoPoint ? ES_SPATIAL_RELATIONS.INTERSECTS : relation, }; @@ -341,6 +419,12 @@ export function createDistanceFilterWithMeta({ geoFieldName, indexPatternId, point, +}: { + alias: string; + distanceKm: number; + geoFieldName: string; + indexPatternId: string; + point: Position; }) { const meta = { type: SPATIAL_FILTER_TYPE, @@ -368,7 +452,7 @@ export function createDistanceFilterWithMeta({ }; } -export function roundCoordinates(coordinates) { +export function roundCoordinates(coordinates: Coordinates): void { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; if (Array.isArray(value)) { @@ -382,10 +466,10 @@ export function roundCoordinates(coordinates) { /* * returns Polygon geometry where coordinates define a bounding box that contains the input geometry */ -export function getBoundingBoxGeometry(geometry) { +export function getBoundingBoxGeometry(geometry: Geometry): Polygon { ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]); - const exterior = geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; + const exterior = (geometry as Polygon).coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; const extent = { minLon: exterior[0][LON_INDEX], minLat: exterior[0][LAT_INDEX], @@ -402,7 +486,7 @@ export function getBoundingBoxGeometry(geometry) { return formatEnvelopeAsPolygon(extent); } -export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { +export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }: MapExtent): Polygon { // GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons // when the shape crosses the dateline const lonDelta = maxLon - minLon; @@ -410,25 +494,25 @@ export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { const right = lonDelta > 360 ? 180 : maxLon; const top = clampToLatBounds(maxLat); const bottom = clampToLatBounds(minLat); - const topLeft = [left, top]; - const bottomLeft = [left, bottom]; - const bottomRight = [right, bottom]; - const topRight = [right, top]; + const topLeft = [left, top] as Position; + const bottomLeft = [left, bottom] as Position; + const bottomRight = [right, bottom] as Position; + const topRight = [right, top] as Position; return { type: GEO_JSON_TYPE.POLYGON, coordinates: [[topLeft, bottomLeft, bottomRight, topRight, topLeft]], - }; + } as Polygon; } -export function clampToLatBounds(lat) { +export function clampToLatBounds(lat: number): number { return clamp(lat, -89, 89); } -export function clampToLonBounds(lon) { +export function clampToLonBounds(lon: number): number { return clamp(lon, -180, 180); } -export function clamp(val, min, max) { +export function clamp(val: number, min: number, max: number): number { if (val > max) { return max; } else if (val < min) { @@ -438,25 +522,26 @@ export function clamp(val, min, max) { } } -export function extractFeaturesFromFilters(filters) { - const features = []; +export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { + const features: Feature[] = []; filters .filter((filter) => { return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; }) .forEach((filter) => { + const geoFieldName = filter.meta.key!; let geometry; - if (filter.geo_distance && filter.geo_distance[filter.meta.key]) { + if (filter.geo_distance && filter.geo_distance[geoFieldName]) { const distanceSplit = filter.geo_distance.distance.split('km'); const distance = parseFloat(distanceSplit[0]); - const circleFeature = turfCircle(filter.geo_distance[filter.meta.key], distance); + const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance); geometry = circleFeature.geometry; } else if ( filter.geo_shape && - filter.geo_shape[filter.meta.key] && - filter.geo_shape[filter.meta.key].shape + filter.geo_shape[geoFieldName] && + filter.geo_shape[geoFieldName].shape ) { - geometry = filter.geo_shape[filter.meta.key].shape; + geometry = filter.geo_shape[geoFieldName].shape; } else { // do not know how to convert spatial filter to geometry // this includes pre-indexed shapes @@ -475,7 +560,7 @@ export function extractFeaturesFromFilters(filters) { return features; } -export function scaleBounds(bounds, scaleFactor) { +export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent { const width = bounds.maxLon - bounds.minLon; const height = bounds.maxLat - bounds.minLat; return { @@ -486,7 +571,7 @@ export function scaleBounds(bounds, scaleFactor) { }; } -export function turfBboxToBounds(turfBbox) { +export function turfBboxToBounds(turfBbox: BBox): MapExtent { return { minLon: turfBbox[0], minLat: turfBbox[1], diff --git a/x-pack/plugins/maps/common/i18n_getters.ts b/x-pack/plugins/maps/common/i18n_getters.ts index 0e1923bb26545..4e9537a12647f 100644 --- a/x-pack/plugins/maps/common/i18n_getters.ts +++ b/x-pack/plugins/maps/common/i18n_getters.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; -import { $Values } from '@kbn/utility-types'; import { ES_SPATIAL_RELATIONS } from './constants'; export function getAppTitle() { @@ -34,7 +33,7 @@ export function getUrlLabel() { }); } -export function getEsSpatialRelationLabel(spatialRelation: $Values) { +export function getEsSpatialRelationLabel(spatialRelation: ES_SPATIAL_RELATIONS) { switch (spatialRelation) { case ES_SPATIAL_RELATIONS.INTERSECTS: return i18n.translate('xpack.maps.common.esSpatialRelation.intersectsLabel', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 6696140a5d852..5ac487c713173 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -206,6 +206,12 @@ describe('ESGeoGridSource', () => { expect(getProperty('filter')).toEqual([ { geo_bounding_box: { bar: { bottom_right: [180, -82.67628], top_left: [-180, 82.67628] } }, + meta: { + alias: null, + disabled: false, + key: 'bar', + negate: false, + }, }, ]); expect(getProperty('aggs')).toEqual({ @@ -277,7 +283,7 @@ describe('ESGeoGridSource', () => { expect(urlTemplateWithMeta.minSourceZoom).toBe(0); expect(urlTemplateWithMeta.maxSourceZoom).toBe(24); expect(urlTemplateWithMeta.urlTemplate).toBe( - "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point" + "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point" ); }); @@ -288,7 +294,7 @@ describe('ESGeoGridSource', () => { }); expect(urlTemplateWithMeta.urlTemplate).toBe( - "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1" + "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1" ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 785b00c06dd54..b3ecdbf51f3c3 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -14,7 +14,12 @@ import { IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties } from 'geojson'; import { AbstractESSource } from '../es_source'; import { getHttp, getSearchService } from '../../../kibana_services'; -import { addFieldToDSL, getField, hitsToGeoJson } from '../../../../common/elasticsearch_util'; +import { + addFieldToDSL, + getField, + hitsToGeoJson, + PreIndexedShape, +} from '../../../../common/elasticsearch_util'; // @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; @@ -43,7 +48,7 @@ import { VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; -import { ImmutableSourceProperty, PreIndexedShape, SourceEditorArgs } from '../source'; +import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; import { GeoJsonWithMeta, diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 6b99f1f8860c0..0936cdc50b4c0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -238,7 +238,6 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource : searchFilters.buffer; const extentFilter = createExtentFilter(buffer, geoField.name); - // @ts-expect-error allFilters.push(extentFilter); } if (searchFilters.applyGlobalTime && (await this.isTimeAware())) { diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index d9eda86428701..7c2aaf714c34e 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -18,6 +18,7 @@ import { FieldFormatter, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { AbstractSourceDescriptor } from '../../../common/descriptor_types'; import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; import { LICENSED_FEATURES } from '../../licensed_features'; +import { PreIndexedShape } from '../../../common/elasticsearch_util'; export type SourceEditorArgs = { onChange: (...args: OnSourceChangeArgs[]) => void; @@ -35,12 +36,6 @@ export type Attribution = { label: string; }; -export type PreIndexedShape = { - index: string; - id: string | number; - path: string; -}; - export interface ISource { destroy(): void; getDisplayName(): Promise; diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/server/mvt/util.ts index 5d75cebb0facd..b3dc606ba3f11 100644 --- a/x-pack/plugins/maps/server/mvt/util.ts +++ b/x-pack/plugins/maps/server/mvt/util.ts @@ -12,10 +12,12 @@ // - only fields from the response are packed in the tile (more efficient) // - query-dsl submitted from the client, which was generated by the IndexPattern // todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26 -import { GeoJsonProperties } from 'geojson'; -export function flattenHit(geometryField: string, hit: Record): GeoJsonProperties { - const flat: GeoJsonProperties = {}; +export function flattenHit( + geometryField: string, + hit: Record +): Record { + const flat: Record = {}; if (hit) { flattenSource(flat, '', hit._source as Record, geometryField); if (hit.fields) { @@ -30,11 +32,11 @@ export function flattenHit(geometryField: string, hit: Record): } function flattenSource( - accum: GeoJsonProperties, + accum: Record, path: string, properties: Record = {}, geometryField: string -): GeoJsonProperties { +): Record { accum = accum || {}; for (const key in properties) { if (properties.hasOwnProperty(key)) { @@ -58,7 +60,7 @@ function flattenSource( return accum; } -function flattenFields(accum: GeoJsonProperties = {}, fields: Array>) { +function flattenFields(accum: Record = {}, fields: Array>) { accum = accum || {}; for (const key in fields) { if (fields.hasOwnProperty(key)) { diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index 7c9623d3e68ec..617000d017025 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -16,6 +16,7 @@ export interface ModuleJob { export interface ModuleDatafeed { id: string; + job_id: string; config: Omit; } @@ -48,7 +49,8 @@ export interface Module { title: string; description: string; type: string; - logoFile: string; + logoFile?: string; + logo?: Logo; defaultIndexPattern: string; query: any; jobs: ModuleJob[]; @@ -56,6 +58,18 @@ export interface Module { kibana: KibanaObjects; } +export interface FileBasedModule extends Omit { + jobs: Array<{ file: string; id: string }>; + datafeeds: Array<{ file: string; job_id: string; id: string }>; + kibana: { + search: Array<{ file: string; id: string }>; + visualization: Array<{ file: string; id: string }>; + dashboard: Array<{ file: string; id: string }>; + }; +} + +export type Logo = { icon: string } | null; + export interface ResultItem { id: string; success?: boolean; diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index f40eefa2167c9..c90707d39ab14 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -7,6 +7,7 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; +export const ML_MODULE_SAVED_OBJECT_TYPE = 'ml-module'; export interface SavedObjectResult { [jobId: string]: { success: boolean; error?: any }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/failures.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/failures.tsx new file mode 100644 index 0000000000000..498320b1b792d --- /dev/null +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/failures.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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { Component } from 'react'; + +import { EuiAccordion, EuiPagination } from '@elastic/eui'; + +const PAGE_SIZE = 100; + +export interface DocFailure { + item: number; + reason: string; + doc: { + message: string; + }; +} + +interface Props { + failedDocs: DocFailure[]; +} + +interface State { + page: number; +} + +export class Failures extends Component { + state: State = { page: 0 }; + + _renderPaginationControl() { + return this.props.failedDocs.length > PAGE_SIZE ? ( + this.setState({ page })} + compressed + /> + ) : null; + } + + render() { + const lastDocIndex = this.props.failedDocs.length - 1; + const startIndex = this.state.page * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE > lastDocIndex ? lastDocIndex : startIndex + PAGE_SIZE; + return ( + + } + paddingSize="m" + > +
+ {this._renderPaginationControl()} + {this.props.failedDocs.slice(startIndex, endIndex).map(({ item, reason, doc }) => ( +
+
+ {item}: {reason} +
+
{JSON.stringify(doc)}
+
+ ))} +
+
+ ); + } +} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.tsx index 46a79044ee5e9..7fa71193ee516 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_summary/import_summary.tsx @@ -8,7 +8,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -import { EuiSpacer, EuiDescriptionList, EuiCallOut, EuiAccordion } from '@elastic/eui'; +import { EuiSpacer, EuiDescriptionList, EuiCallOut } from '@elastic/eui'; +import { DocFailure, Failures } from './failures'; interface Props { index: string; @@ -20,14 +21,6 @@ interface Props { createPipeline: boolean; } -interface DocFailure { - item: number; - reason: string; - doc: { - message: string; - }; -} - export const ImportSummary: FC = ({ index, indexPattern, @@ -96,36 +89,6 @@ export const ImportSummary: FC = ({ ); }; -interface FailuresProps { - failedDocs: DocFailure[]; -} - -const Failures: FC = ({ failedDocs }) => { - return ( - - } - paddingSize="m" - > -
- {failedDocs.map(({ item, reason, doc }) => ( -
-
- {item}: {reason} -
-
{JSON.stringify(doc)}
-
- ))} -
-
- ); -}; - function createDisplayItems( index: string, indexPattern: string, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx index cd594d25578cd..e577587b01baf 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/scroll_size/description.tsx @@ -25,7 +25,7 @@ export const Description: FC = memo(({ children, validation }) => { description={ } > diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index a1fac92d45b4e..4e99330610fca 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -9,6 +9,7 @@ import fs from 'fs'; import Boom from '@hapi/boom'; import numeral from '@elastic/numeral'; import { KibanaRequest, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; + import moment from 'moment'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; @@ -16,12 +17,15 @@ import { AnalysisLimits } from '../../../common/types/anomaly_detection_jobs'; import { getAuthorizationHeader } from '../../lib/request_authorization'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import type { MlClient } from '../../lib/ml_client'; +import { ML_MODULE_SAVED_OBJECT_TYPE } from '../../../common/types/saved_objects'; import { KibanaObjects, KibanaObjectConfig, ModuleDatafeed, ModuleJob, Module, + FileBasedModule, + Logo, JobOverride, DatafeedOverride, GeneralJobsOverride, @@ -45,7 +49,10 @@ import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; import { JobExistResult, JobStat } from '../../../common/types/data_recognizer'; import { MlJobsStatsResponse } from '../../../common/types/job_service'; +import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; import { JobSavedObjectService } from '../../saved_objects'; +import { isDefined } from '../../../common/types/guards'; +import { isPopulatedObject } from '../../../common/util/object_utils'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; @@ -57,26 +64,18 @@ export const SAVED_OBJECT_TYPES = { VISUALIZATION: 'visualization', }; -interface RawModuleConfig { - id: string; - title: string; - description: string; - type: string; - logoFile: string; - defaultIndexPattern: string; - query: any; - jobs: Array<{ file: string; id: string }>; - datafeeds: Array<{ file: string; job_id: string; id: string }>; - kibana: { - search: Array<{ file: string; id: string }>; - visualization: Array<{ file: string; id: string }>; - dashboard: Array<{ file: string; id: string }>; - }; +function isModule(arg: unknown): arg is Module { + return isPopulatedObject(arg) && Array.isArray(arg.jobs) && arg.jobs[0]?.config !== undefined; +} + +function isFileBasedModule(arg: unknown): arg is FileBasedModule { + return isPopulatedObject(arg) && Array.isArray(arg.jobs) && arg.jobs[0]?.file !== undefined; } interface Config { - dirName: any; - json: RawModuleConfig; + dirName?: string; + module: FileBasedModule | Module; + isSavedObject: boolean; } export interface RecognizeResult { @@ -84,7 +83,7 @@ export interface RecognizeResult { title: string; query: any; description: string; - logo: { icon: string } | null; + logo: Logo; } interface ObjectExistResult { @@ -125,7 +124,7 @@ export class DataRecognizer { /** * List of the module jobs that require model memory estimation */ - jobsForModelMemoryEstimation: Array<{ job: ModuleJob; query: any }> = []; + private _jobsForModelMemoryEstimation: Array<{ job: ModuleJob; query: any }> = []; constructor( mlClusterClient: IScopedClusterClient, @@ -146,7 +145,7 @@ export class DataRecognizer { } // list all directories under the given directory - async listDirs(dirName: string): Promise { + private async _listDirs(dirName: string): Promise { const dirs: string[] = []; return new Promise((resolve, reject) => { fs.readdir(dirName, (err, fileNames) => { @@ -164,7 +163,7 @@ export class DataRecognizer { }); } - async readFile(fileName: string): Promise { + private async _readFile(fileName: string): Promise { return new Promise((resolve, reject) => { fs.readFile(fileName, 'utf-8', (err, content) => { if (err) { @@ -176,14 +175,14 @@ export class DataRecognizer { }); } - async loadManifestFiles(): Promise { + private async _loadConfigs(): Promise { const configs: Config[] = []; - const dirs = await this.listDirs(this._modulesDir); + const dirs = await this._listDirs(this._modulesDir); await Promise.all( dirs.map(async (dir) => { let file: string | undefined; try { - file = await this.readFile(`${this._modulesDir}/${dir}/manifest.json`); + file = await this._readFile(`${this._modulesDir}/${dir}/manifest.json`); } catch (error) { mlLog.warn(`Data recognizer skipping folder ${dir} as manifest.json cannot be read`); } @@ -192,7 +191,8 @@ export class DataRecognizer { try { configs.push({ dirName: dir, - json: JSON.parse(file), + module: JSON.parse(file), + isSavedObject: false, }); } catch (error) { mlLog.warn(`Data recognizer error parsing ${dir}/manifest.json. ${error}`); @@ -201,26 +201,40 @@ export class DataRecognizer { }) ); - return configs; + const savedObjectConfigs = (await this._loadSavedObjectModules()).map((module) => ({ + module, + isSavedObject: true, + })); + + return [...configs, ...savedObjectConfigs]; + } + + private async _loadSavedObjectModules() { + const jobs = await this._savedObjectsClient.find({ + type: ML_MODULE_SAVED_OBJECT_TYPE, + perPage: 10000, + }); + + return jobs.saved_objects.map((o) => o.attributes); } // get the manifest.json file for a specified id, e.g. "nginx" - async getManifestFile(id: string) { - const manifestFiles = await this.loadManifestFiles(); - return manifestFiles.find((i) => i.json.id === id); + private async _findConfig(id: string) { + const configs = await this._loadConfigs(); + return configs.find((i) => i.module.id === id); } // called externally by an endpoint - async findMatches(indexPattern: string): Promise { - const manifestFiles = await this.loadManifestFiles(); + public async findMatches(indexPattern: string): Promise { + const manifestFiles = await this._loadConfigs(); const results: RecognizeResult[] = []; await Promise.all( manifestFiles.map(async (i) => { - const moduleConfig = i.json; + const moduleConfig = i.module; let match = false; try { - match = await this.searchForFields(moduleConfig, indexPattern); + match = await this._searchForFields(moduleConfig, indexPattern); } catch (error) { mlLog.warn( `Data recognizer error running query defined for module ${moduleConfig.id}. ${error}` @@ -228,13 +242,15 @@ export class DataRecognizer { } if (match === true) { - let logo = null; - if (moduleConfig.logoFile) { + let logo: Logo = null; + if (moduleConfig.logo) { + logo = moduleConfig.logo; + } else if (moduleConfig.logoFile) { try { - logo = await this.readFile( + const logoFile = await this._readFile( `${this._modulesDir}/${i.dirName}/${moduleConfig.logoFile}` ); - logo = JSON.parse(logo); + logo = JSON.parse(logoFile); } catch (e) { logo = null; } @@ -255,7 +271,7 @@ export class DataRecognizer { return results; } - async searchForFields(moduleConfig: RawModuleConfig, indexPattern: string) { + private async _searchForFields(moduleConfig: FileBasedModule | Module, indexPattern: string) { if (moduleConfig.query === undefined) { return false; } @@ -275,29 +291,34 @@ export class DataRecognizer { return body.hits.total.value > 0; } - async listModules() { - const manifestFiles = await this.loadManifestFiles(); - const ids = manifestFiles.map(({ json }) => json.id).sort((a, b) => a.localeCompare(b)); // sort as json files are read from disk and could be in any order. + public async listModules() { + const manifestFiles = await this._loadConfigs(); + manifestFiles.sort((a, b) => a.module.id.localeCompare(b.module.id)); // sort as json files are read from disk and could be in any order. - const modules = []; - for (let i = 0; i < ids.length; i++) { - const module = await this.getModule(ids[i]); - modules.push(module); + const configs: Array = []; + for (const config of manifestFiles) { + if (config.isSavedObject) { + configs.push(config.module); + } else { + configs.push(await this.getModule(config.module.id)); + } } - return modules; + // casting return as Module[] so not to break external plugins who rely on this function + // once FileBasedModules are removed this function will only deal with Modules + return configs as Module[]; } // called externally by an endpoint // supplying an optional prefix will add the prefix // to the job and datafeed configs - async getModule(id: string, prefix = ''): Promise { - let manifestJSON: RawModuleConfig | null = null; + public async getModule(id: string, prefix = ''): Promise { + let module: FileBasedModule | Module | null = null; let dirName: string | null = null; - const manifestFile = await this.getManifestFile(id); - if (manifestFile !== undefined) { - manifestJSON = manifestFile.json; - dirName = manifestFile.dirName; + const config = await this._findConfig(id); + if (config !== undefined) { + module = config.module; + dirName = config.dirName ?? null; } else { throw Boom.notFound(`Module with the id "${id}" not found`); } @@ -306,81 +327,102 @@ export class DataRecognizer { const datafeeds: ModuleDatafeed[] = []; const kibana: KibanaObjects = {}; // load all of the job configs - await Promise.all( - manifestJSON.jobs.map(async (job) => { + if (isModule(module)) { + const tempJobs: ModuleJob[] = module.jobs.map((j) => ({ + id: `${prefix}${j.id}`, + config: j.config, + })); + jobs.push(...tempJobs); + const tempDatafeeds: ModuleDatafeed[] = module.datafeeds.map((d) => { + const jobId = `${prefix}${d.job_id}`; + return { + id: prefixDatafeedId(d.id, prefix), + job_id: jobId, + config: { + ...d.config, + job_id: jobId, + }, + }; + }); + datafeeds.push(...tempDatafeeds); + } else if (isFileBasedModule(module)) { + const tempJobs = module.jobs.map(async (job) => { try { - const jobConfig = await this.readFile( + const jobConfig = await this._readFile( `${this._modulesDir}/${dirName}/${ML_DIR}/${job.file}` ); // use the file name for the id - jobs.push({ + return { id: `${prefix}${job.id}`, config: JSON.parse(jobConfig), - }); + }; } catch (error) { mlLog.warn( `Data recognizer error loading config for job ${job.id} for module ${id}. ${error}` ); } - }) - ); + }); + jobs.push(...(await Promise.all(tempJobs)).filter(isDefined)); - // load all of the datafeed configs - await Promise.all( - manifestJSON.datafeeds.map(async (datafeed) => { + // load all of the datafeed configs + const tempDatafeed = module.datafeeds.map(async (datafeed) => { try { - const datafeedConfig = await this.readFile( + const datafeedConfigString = await this._readFile( `${this._modulesDir}/${dirName}/${ML_DIR}/${datafeed.file}` ); - const config = JSON.parse(datafeedConfig); - // use the job id from the manifestFile - config.job_id = `${prefix}${datafeed.job_id}`; + const datafeedConfig = JSON.parse(datafeedConfigString) as Datafeed; + // use the job id from the module + datafeedConfig.job_id = `${prefix}${datafeed.job_id}`; - datafeeds.push({ + return { id: prefixDatafeedId(datafeed.id, prefix), - config, - }); + job_id: datafeedConfig.job_id, + config: datafeedConfig, + }; } catch (error) { mlLog.warn( `Data recognizer error loading config for datafeed ${datafeed.id} for module ${id}. ${error}` ); } - }) - ); + }); + datafeeds.push(...(await Promise.all(tempDatafeed)).filter(isDefined)); + } // load all of the kibana saved objects - if (manifestJSON.kibana !== undefined) { - const kKeys = Object.keys(manifestJSON.kibana) as Array; + if (module.kibana !== undefined) { + const kKeys = Object.keys(module.kibana) as Array; await Promise.all( kKeys.map(async (key) => { kibana[key] = []; - await Promise.all( - manifestJSON!.kibana[key].map(async (obj) => { - try { - const kConfig = await this.readFile( - `${this._modulesDir}/${dirName}/${KIBANA_DIR}/${key}/${obj.file}` - ); - // use the file name for the id - const kId = obj.file.replace('.json', ''); - const config = JSON.parse(kConfig); - kibana[key]!.push({ - id: kId, - title: config.title, - config, - }); - } catch (error) { - mlLog.warn( - `Data recognizer error loading config for ${key} ${obj.id} for module ${id}. ${error}` - ); - } - }) - ); + if (isFileBasedModule(module)) { + await Promise.all( + module.kibana[key].map(async (obj) => { + try { + const kConfigString = await this._readFile( + `${this._modulesDir}/${dirName}/${KIBANA_DIR}/${key}/${obj.file}` + ); + // use the file name for the id + const kId = obj.file.replace('.json', ''); + const kConfig = JSON.parse(kConfigString); + kibana[key]!.push({ + id: kId, + title: kConfig.title, + config: kConfig, + }); + } catch (error) { + mlLog.warn( + `Data recognizer error loading config for ${key} ${obj.id} for module ${id}. ${error}` + ); + } + }) + ); + } }) ); } return { - ...manifestJSON, + ...module, jobs, datafeeds, kibana, @@ -391,7 +433,7 @@ export class DataRecognizer { // takes a module config id, an optional jobPrefix and the request object // creates all of the jobs, datafeeds and savedObjects listed in the module config. // if any of the savedObjects already exist, they will not be overwritten. - async setup( + public async setup( moduleId: string, jobPrefix?: string, groups?: string[], @@ -417,11 +459,11 @@ export class DataRecognizer { this._indexPatternName = indexPatternName === undefined ? moduleConfig.defaultIndexPattern : indexPatternName; - this._indexPatternId = await this.getIndexPatternId(this._indexPatternName); + this._indexPatternId = await this._getIndexPatternId(this._indexPatternName); // the module's jobs contain custom URLs which require an index patten id // but there is no corresponding index pattern, throw an error - if (this._indexPatternId === undefined && this.doJobUrlsContainIndexPatternId(moduleConfig)) { + if (this._indexPatternId === undefined && this._doJobUrlsContainIndexPatternId(moduleConfig)) { throw Boom.badRequest( `Module's jobs contain custom URLs which require a kibana index pattern (${this._indexPatternName}) which cannot be found.` ); @@ -431,7 +473,7 @@ export class DataRecognizer { // but there is no corresponding index pattern, throw an error if ( this._indexPatternId === undefined && - this.doSavedObjectsContainIndexPatternId(moduleConfig) + this._doSavedObjectsContainIndexPatternId(moduleConfig) ) { throw Boom.badRequest( `Module's saved objects contain custom URLs which require a kibana index pattern (${this._indexPatternName}) which cannot be found.` @@ -439,23 +481,23 @@ export class DataRecognizer { } // create an empty results object - const results = this.createResultsTemplate(moduleConfig); + const results = this._createResultsTemplate(moduleConfig); const saveResults: SaveResults = { jobs: [] as JobResponse[], datafeeds: [] as DatafeedResponse[], savedObjects: [] as KibanaObjectResponse[], }; - this.jobsForModelMemoryEstimation = moduleConfig.jobs.map((job) => ({ + this._jobsForModelMemoryEstimation = moduleConfig.jobs.map((job) => ({ job, query: moduleConfig.datafeeds.find((d) => d.config.job_id === job.id)?.config.query ?? null, })); this.applyJobConfigOverrides(moduleConfig, jobOverrides, jobPrefix); this.applyDatafeedConfigOverrides(moduleConfig, datafeedOverrides, jobPrefix); - this.updateDatafeedIndices(moduleConfig); - this.updateJobUrlIndexPatterns(moduleConfig); - await this.updateModelMemoryLimits(moduleConfig, estimateModelMemory, start, end); + this._updateDatafeedIndices(moduleConfig); + this._updateJobUrlIndexPatterns(moduleConfig); + await this._updateModelMemoryLimits(moduleConfig, estimateModelMemory, start, end); // create the jobs if (moduleConfig.jobs && moduleConfig.jobs.length) { @@ -468,7 +510,7 @@ export class DataRecognizer { if (useDedicatedIndex === true) { moduleConfig.jobs.forEach((job) => (job.config.results_index_name = job.id)); } - saveResults.jobs = await this.saveJobs(moduleConfig.jobs, applyToAllSpaces); + saveResults.jobs = await this._saveJobs(moduleConfig.jobs, applyToAllSpaces); } // create the datafeeds @@ -478,7 +520,7 @@ export class DataRecognizer { df.config.query = query; }); } - saveResults.datafeeds = await this.saveDatafeeds(moduleConfig.datafeeds); + saveResults.datafeeds = await this._saveDatafeeds(moduleConfig.datafeeds); if (startDatafeed) { const savedDatafeeds = moduleConfig.datafeeds.filter((df) => { @@ -486,7 +528,7 @@ export class DataRecognizer { return datafeedResult !== undefined && datafeedResult.success === true; }); - const startResults = await this.startDatafeeds(savedDatafeeds, start, end); + const startResults = await this._startDatafeeds(savedDatafeeds, start, end); saveResults.datafeeds.forEach((df) => { const startedDatafeed = startResults[df.id]; if (startedDatafeed !== undefined) { @@ -503,26 +545,26 @@ export class DataRecognizer { // create the savedObjects if (moduleConfig.kibana) { // update the saved objects with the index pattern id - this.updateSavedObjectIndexPatterns(moduleConfig); + this._updateSavedObjectIndexPatterns(moduleConfig); - const savedObjects = await this.createSavedObjectsToSave(moduleConfig); + const savedObjects = await this._createSavedObjectsToSave(moduleConfig); // update the exists flag in the results - this.updateKibanaResults(results.kibana, savedObjects); + this._updateKibanaResults(results.kibana, savedObjects); // create the savedObjects try { - saveResults.savedObjects = await this.saveKibanaObjects(savedObjects); + saveResults.savedObjects = await this._saveKibanaObjects(savedObjects); } catch (error) { // only one error is returned for the bulk create saved object request // so populate every saved object with the same error. - this.populateKibanaResultErrors(results.kibana, error.output?.payload); + this._populateKibanaResultErrors(results.kibana, error.output?.payload); } } // merge all the save results - this.updateResults(results, saveResults); + this._updateResults(results, saveResults); return results; } - async dataRecognizerJobsExist(moduleId: string): Promise { + public async dataRecognizerJobsExist(moduleId: string): Promise { const results = {} as JobExistResult; // Load the module with the specified ID and check if the jobs @@ -573,7 +615,7 @@ export class DataRecognizer { return results; } - async loadIndexPatterns() { + private async _loadIndexPatterns() { return await this._savedObjectsClient.find({ type: 'index-pattern', perPage: 1000, @@ -581,9 +623,9 @@ export class DataRecognizer { } // returns a id based on an index pattern name - async getIndexPatternId(name: string) { + private async _getIndexPatternId(name: string) { try { - const indexPatterns = await this.loadIndexPatterns(); + const indexPatterns = await this._loadIndexPatterns(); if (indexPatterns === undefined || indexPatterns.saved_objects === undefined) { return; } @@ -598,9 +640,9 @@ export class DataRecognizer { // create a list of objects which are used to save the savedObjects. // each has an exists flag and those which do not already exist // contain a savedObject object which is sent to the server to save - async createSavedObjectsToSave(moduleConfig: Module) { + private async _createSavedObjectsToSave(moduleConfig: Module) { // first check if the saved objects already exist. - const savedObjectExistResults = await this.checkIfSavedObjectsExist(moduleConfig.kibana); + const savedObjectExistResults = await this._checkIfSavedObjectsExist(moduleConfig.kibana); // loop through the kibanaSaveResults and update Object.keys(moduleConfig.kibana).forEach((type) => { // type e.g. dashboard, search ,visualization @@ -624,7 +666,7 @@ export class DataRecognizer { } // update the exists flags in the kibana results - updateKibanaResults( + private _updateKibanaResults( kibanaSaveResults: DataRecognizerConfigResponse['kibana'], objectExistResults: ObjectExistResult[] ) { @@ -640,7 +682,7 @@ export class DataRecognizer { // add an error object to every kibana saved object, // if it doesn't already exist. - populateKibanaResultErrors( + private _populateKibanaResultErrors( kibanaSaveResults: DataRecognizerConfigResponse['kibana'], error: any ) { @@ -661,11 +703,13 @@ export class DataRecognizer { // load existing savedObjects for each type and compare to find out if // items with the same id already exist. // returns a flat list of objects with exists flags set - async checkIfSavedObjectsExist(kibanaObjects: KibanaObjects): Promise { + private async _checkIfSavedObjectsExist( + kibanaObjects: KibanaObjects + ): Promise { const types = Object.keys(kibanaObjects); const results: ObjectExistResponse[][] = await Promise.all( types.map(async (type) => { - const existingObjects = await this.loadExistingSavedObjects(type); + const existingObjects = await this._loadExistingSavedObjects(type); return kibanaObjects[type]!.map((obj) => { const existingObject = existingObjects.saved_objects.find( (o) => o.attributes && o.attributes.title === obj.title @@ -683,13 +727,13 @@ export class DataRecognizer { } // find all existing savedObjects for a given type - loadExistingSavedObjects(type: string) { + private _loadExistingSavedObjects(type: string) { // TODO: define saved object type return this._savedObjectsClient.find({ type, perPage: 1000 }); } // save the savedObjects if they do not exist already - async saveKibanaObjects(objectExistResults: ObjectExistResponse[]) { + private async _saveKibanaObjects(objectExistResults: ObjectExistResponse[]) { let results = { saved_objects: [] as any[] }; const filteredSavedObjects = objectExistResults .filter((o) => o.exists === false) @@ -710,13 +754,16 @@ export class DataRecognizer { // save the jobs. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveJobs(jobs: ModuleJob[], applyToAllSpaces: boolean = false): Promise { + private async _saveJobs( + jobs: ModuleJob[], + applyToAllSpaces: boolean = false + ): Promise { const resp = await Promise.all( jobs.map(async (job) => { const jobId = job.id; try { job.id = jobId; - await this.saveJob(job); + await this._saveJob(job); return { id: jobId, success: true }; } catch ({ body }) { return { id: jobId, success: false, error: body }; @@ -738,18 +785,18 @@ export class DataRecognizer { return resp; } - async saveJob(job: ModuleJob) { + private async _saveJob(job: ModuleJob) { return this._mlClient.putJob({ job_id: job.id, body: job.config }); } // save the datafeeds. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveDatafeeds(datafeeds: ModuleDatafeed[]) { + private async _saveDatafeeds(datafeeds: ModuleDatafeed[]) { return await Promise.all( datafeeds.map(async (datafeed) => { try { - await this.saveDatafeed(datafeed); + await this._saveDatafeed(datafeed); return { id: datafeed.id, success: true, @@ -769,7 +816,7 @@ export class DataRecognizer { ); } - async saveDatafeed(datafeed: ModuleDatafeed) { + private async _saveDatafeed(datafeed: ModuleDatafeed) { return this._mlClient.putDatafeed( { datafeed_id: datafeed.id, @@ -779,19 +826,19 @@ export class DataRecognizer { ); } - async startDatafeeds( + private async _startDatafeeds( datafeeds: ModuleDatafeed[], start?: number, end?: number ): Promise<{ [key: string]: DatafeedResponse }> { const results = {} as { [key: string]: DatafeedResponse }; for (const datafeed of datafeeds) { - results[datafeed.id] = await this.startDatafeed(datafeed, start, end); + results[datafeed.id] = await this._startDatafeed(datafeed, start, end); } return results; } - async startDatafeed( + private async _startDatafeed( datafeed: ModuleDatafeed, start: number | undefined, end: number | undefined @@ -845,7 +892,7 @@ export class DataRecognizer { // merge all of the save results into one result object // which is returned from the endpoint - async updateResults(results: DataRecognizerConfigResponse, saveResults: SaveResults) { + private async _updateResults(results: DataRecognizerConfigResponse, saveResults: SaveResults) { // update job results results.jobs.forEach((j) => { saveResults.jobs.forEach((j2) => { @@ -894,7 +941,7 @@ export class DataRecognizer { // creates an empty results object, // listing each job/datafeed/savedObject with a save success boolean - createResultsTemplate(moduleConfig: Module): DataRecognizerConfigResponse { + private _createResultsTemplate(moduleConfig: Module): DataRecognizerConfigResponse { const results: DataRecognizerConfigResponse = {} as DataRecognizerConfigResponse; const reducedConfig = { jobs: moduleConfig.jobs, @@ -932,7 +979,7 @@ export class DataRecognizer { // if an override index pattern has been specified, // update all of the datafeeds. - updateDatafeedIndices(moduleConfig: Module) { + private _updateDatafeedIndices(moduleConfig: Module) { // if the supplied index pattern contains a comma, split into multiple indices and // add each one to the datafeed const indexPatternNames = splitIndexPatternNames(this._indexPatternName); @@ -962,7 +1009,7 @@ export class DataRecognizer { // loop through the custom urls in each job and replace the INDEX_PATTERN_ID // marker for the id of the specified index pattern - updateJobUrlIndexPatterns(moduleConfig: Module) { + private _updateJobUrlIndexPatterns(moduleConfig: Module) { if (Array.isArray(moduleConfig.jobs)) { moduleConfig.jobs.forEach((job) => { // if the job has custom_urls @@ -986,7 +1033,7 @@ export class DataRecognizer { // check the custom urls in the module's jobs to see if they contain INDEX_PATTERN_ID // which needs replacement - doJobUrlsContainIndexPatternId(moduleConfig: Module) { + private _doJobUrlsContainIndexPatternId(moduleConfig: Module) { if (Array.isArray(moduleConfig.jobs)) { for (const job of moduleConfig.jobs) { // if the job has custom_urls @@ -1004,7 +1051,7 @@ export class DataRecognizer { // loop through each kibana saved object and replace any INDEX_PATTERN_ID and // INDEX_PATTERN_NAME markers for the id or name of the specified index pattern - updateSavedObjectIndexPatterns(moduleConfig: Module) { + private _updateSavedObjectIndexPatterns(moduleConfig: Module) { if (moduleConfig.kibana) { Object.keys(moduleConfig.kibana).forEach((category) => { moduleConfig.kibana[category]!.forEach((item) => { @@ -1037,7 +1084,7 @@ export class DataRecognizer { /** * Provides a time range of the last 3 months of data */ - async getFallbackTimeRange( + private async _getFallbackTimeRange( timeField: string, query?: any ): Promise<{ start: number; end: number }> { @@ -1059,7 +1106,7 @@ export class DataRecognizer { * Ensure the model memory limit for each job is not greater than * the max model memory setting for the cluster */ - async updateModelMemoryLimits( + private async _updateModelMemoryLimits( moduleConfig: Module, estimateMML: boolean, start?: number, @@ -1069,12 +1116,12 @@ export class DataRecognizer { return; } - if (estimateMML && this.jobsForModelMemoryEstimation.length > 0) { + if (estimateMML && this._jobsForModelMemoryEstimation.length > 0) { try { // Checks if all jobs in the module have the same time field configured - const firstJobTimeField = this.jobsForModelMemoryEstimation[0].job.config.data_description + const firstJobTimeField = this._jobsForModelMemoryEstimation[0].job.config.data_description .time_field; - const isSameTimeFields = this.jobsForModelMemoryEstimation.every( + const isSameTimeFields = this._jobsForModelMemoryEstimation.every( ({ job }) => job.config.data_description.time_field === firstJobTimeField ); @@ -1085,16 +1132,16 @@ export class DataRecognizer { const { start: fallbackStart, end: fallbackEnd, - } = await this.getFallbackTimeRange(firstJobTimeField, { match_all: {} }); + } = await this._getFallbackTimeRange(firstJobTimeField, { match_all: {} }); start = fallbackStart; end = fallbackEnd; } - for (const { job, query } of this.jobsForModelMemoryEstimation) { + for (const { job, query } of this._jobsForModelMemoryEstimation) { let earliestMs = start; let latestMs = end; if (earliestMs === undefined || latestMs === undefined) { - const timeFieldRange = await this.getFallbackTimeRange( + const timeFieldRange = await this._getFallbackTimeRange( job.config.data_description.time_field, query ); @@ -1157,7 +1204,7 @@ export class DataRecognizer { // check the kibana saved searches JSON in the module to see if they contain INDEX_PATTERN_ID // which needs replacement - doSavedObjectsContainIndexPatternId(moduleConfig: Module) { + private _doSavedObjectsContainIndexPatternId(moduleConfig: Module) { if (moduleConfig.kibana) { for (const category of Object.keys(moduleConfig.kibana)) { for (const item of moduleConfig.kibana[category]!) { @@ -1171,7 +1218,7 @@ export class DataRecognizer { return false; } - applyJobConfigOverrides( + public applyJobConfigOverrides( moduleConfig: Module, jobOverrides?: JobOverride | JobOverride[], jobPrefix = '' @@ -1205,9 +1252,9 @@ export class DataRecognizer { }); if (generalOverrides.some((override) => !!override.analysis_limits?.model_memory_limit)) { - this.jobsForModelMemoryEstimation = []; + this._jobsForModelMemoryEstimation = []; } else { - this.jobsForModelMemoryEstimation = moduleConfig.jobs + this._jobsForModelMemoryEstimation = moduleConfig.jobs .filter((job) => { const override = jobSpecificOverrides.find((o) => `${jobPrefix}${o.job_id}` === job.id); return override?.analysis_limits?.model_memory_limit === undefined; @@ -1266,7 +1313,7 @@ export class DataRecognizer { }); } - applyDatafeedConfigOverrides( + public applyDatafeedConfigOverrides( moduleConfig: Module, datafeedOverrides?: DatafeedOverride | DatafeedOverride[], jobPrefix = '' diff --git a/x-pack/plugins/ml/server/saved_objects/mappings.json b/x-pack/plugins/ml/server/saved_objects/mappings.json deleted file mode 100644 index 9a23dba324dbf..0000000000000 --- a/x-pack/plugins/ml/server/saved_objects/mappings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "job": { - "properties": { - "job_id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "datafeed_id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - } - } - } -} diff --git a/x-pack/plugins/ml/server/saved_objects/mappings.ts b/x-pack/plugins/ml/server/saved_objects/mappings.ts new file mode 100644 index 0000000000000..f452991015723 --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/mappings.ts @@ -0,0 +1,90 @@ +/* + * 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 { SavedObjectsTypeMappingDefinition } from 'kibana/server'; + +export const mlJob: SavedObjectsTypeMappingDefinition = { + properties: { + job_id: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + datafeed_id: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + type: { + type: 'keyword', + }, + }, +}; + +export const mlModule: SavedObjectsTypeMappingDefinition = { + dynamic: false, + properties: { + id: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + title: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + description: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + type: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + logo: { + type: 'object', + }, + defaultIndexPattern: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + query: { + type: 'object', + }, + jobs: { + type: 'object', + }, + datafeeds: { + type: 'object', + }, + }, +}; diff --git a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts index e30ff60960e27..004b5e8e554cc 100644 --- a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts +++ b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts @@ -6,10 +6,13 @@ */ import { SavedObjectsServiceSetup } from 'kibana/server'; -import mappings from './mappings.json'; +import { mlJob, mlModule } from './mappings'; import { migrations } from './migrations'; -import { ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; +import { + ML_SAVED_OBJECT_TYPE, + ML_MODULE_SAVED_OBJECT_TYPE, +} from '../../common/types/saved_objects'; export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { savedObjects.registerType({ @@ -17,6 +20,13 @@ export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { hidden: false, namespaceType: 'multiple', migrations, - mappings: mappings.job, + mappings: mlJob, + }); + savedObjects.registerType({ + name: ML_MODULE_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + migrations, + mappings: mlModule, }); } diff --git a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx index d45ba91bb35fb..4e9f1a6692eb9 100644 --- a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -62,7 +62,7 @@ describe('', () => { }); expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual( - user.full_name + `Settings for ${user.full_name}` ); expect(wrapper.find('[data-test-subj="username"]').text()).toEqual(user.username); expect(wrapper.find('[data-test-subj="email"]').text()).toEqual(user.email); @@ -83,7 +83,9 @@ describe('', () => { wrapper.update(); }); - expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(user.username); + expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual( + `Settings for ${user.username}` + ); }); it(`displays a placeholder when no email address is provided`, async () => { diff --git a/x-pack/plugins/security/public/account_management/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx index c8fe80e254a46..60f48c01a6ff7 100644 --- a/x-pack/plugins/security/public/account_management/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -9,6 +9,7 @@ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { CoreStart, NotificationsStart } from 'src/core/public'; @@ -40,7 +41,13 @@ export const AccountManagementPage = ({ userAPIClient, authc, notifications }: P -

{getUserDisplayName(currentUser)}

+

+ {getUserDisplayName(currentUser)} }} + /> +

diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx index bd338109a4460..f2d3fcd6ab3ca 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiHeaderSectionItemButton, EuiPopover } from '@elastic/eui'; +import { EuiContextMenuItem, EuiHeaderSectionItemButton, EuiPopover } from '@elastic/eui'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; @@ -181,4 +181,58 @@ describe('SecurityNavControl', () => { expect(findTestSubject(wrapper, 'logoutLink').text()).toBe('Log in'); }); + + it('properly renders without a custom profile link.', async () => { + const props = { + user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })), + editProfileUrl: '', + logoutUrl: '', + userMenuLinks$: new BehaviorSubject([ + { label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 }, + { label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2 }, + ]), + }; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([]); + + wrapper.find(EuiHeaderSectionItemButton).simulate('click'); + + expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([ + 'Profile', + 'link1', + 'link2', + 'Log out', + ]); + }); + + it('properly renders with a custom profile link.', async () => { + const props = { + user: Promise.resolve(mockAuthenticatedUser({ full_name: 'foo' })), + editProfileUrl: '', + logoutUrl: '', + userMenuLinks$: new BehaviorSubject([ + { label: 'link1', href: 'path-to-link-1', iconType: 'empty', order: 1 }, + { label: 'link2', href: 'path-to-link-2', iconType: 'empty', order: 2, setAsProfile: true }, + ]), + }; + + const wrapper = mountWithIntl(); + await nextTick(); + wrapper.update(); + + expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([]); + + wrapper.find(EuiHeaderSectionItemButton).simulate('click'); + + expect(wrapper.find(EuiContextMenuItem).map((node) => node.text())).toEqual([ + 'link1', + 'link2', + 'Preferences', + 'Log out', + ]); + }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index c7649494bb810..546d6cf5a6bba 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -30,6 +30,7 @@ export interface UserMenuLink { iconType: IconType; href: string; order?: number; + setAsProfile?: boolean; } interface Props { @@ -123,35 +124,39 @@ export class SecurityNavControl extends Component { const isAnonymousUser = authenticatedUser?.authentication_provider.type === 'anonymous'; const items: EuiContextMenuPanelItemDescriptor[] = []; + if (userMenuLinks.length) { + const userMenuLinkMenuItems = userMenuLinks + .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) + .map(({ label, iconType, href }: UserMenuLink) => ({ + name: {label}, + icon: , + href, + 'data-test-subj': `userMenuLink__${label}`, + })); + items.push(...userMenuLinkMenuItems); + } + if (!isAnonymousUser) { + const hasCustomProfileLinks = userMenuLinks.some(({ setAsProfile }) => setAsProfile === true); const profileMenuItem = { name: ( ), - icon: , + icon: , href: editProfileUrl, 'data-test-subj': 'profileLink', }; - items.push(profileMenuItem); - } - if (userMenuLinks.length) { - const userMenuLinkMenuItems = userMenuLinks - .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) - .map(({ label, iconType, href }: UserMenuLink) => ({ - name: {label}, - icon: , - href, - 'data-test-subj': `userMenuLink__${label}`, - })); - - items.push(...userMenuLinkMenuItems, { - isSeparator: true, - key: 'securityNavControlComponent__userMenuLinksSeparator', - }); + // Set this as the first link if there is no user-defined profile link + if (!hasCustomProfileLinks) { + items.unshift(profileMenuItem); + } else { + items.push(profileMenuItem); + } } const logoutMenuItem = { diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts index 72a1a6f5817a5..035177d78c9c6 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.test.ts @@ -178,16 +178,19 @@ describe('SecurityNavControlService', () => { }); describe(`#start`, () => { - it('should return functions to register and retrieve user menu links', () => { - const license$ = new BehaviorSubject(validLicense); + let navControlService: SecurityNavControlService; + beforeEach(() => { + const license$ = new BehaviorSubject({} as ILicense); - const navControlService = new SecurityNavControlService(); + navControlService = new SecurityNavControlService(); navControlService.setup({ securityLicense: new SecurityLicenseService().setup({ license$ }).license, authc: securityMock.createSetup().authc, logoutUrl: '/some/logout/url', }); + }); + it('should return functions to register and retrieve user menu links', () => { const coreStart = coreMock.createStart(); const navControlServiceStart = navControlService.start({ core: coreStart }); expect(navControlServiceStart).toHaveProperty('getUserMenuLinks$'); @@ -195,15 +198,6 @@ describe('SecurityNavControlService', () => { }); it('should register custom user menu links to be displayed in the nav controls', (done) => { - const license$ = new BehaviorSubject(validLicense); - - const navControlService = new SecurityNavControlService(); - navControlService.setup({ - securityLicense: new SecurityLicenseService().setup({ license$ }).license, - authc: securityMock.createSetup().authc, - logoutUrl: '/some/logout/url', - }); - const coreStart = coreMock.createStart(); const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart }); const userMenuLinks$ = getUserMenuLinks$(); @@ -231,15 +225,6 @@ describe('SecurityNavControlService', () => { }); it('should retrieve user menu links sorted by order', (done) => { - const license$ = new BehaviorSubject(validLicense); - - const navControlService = new SecurityNavControlService(); - navControlService.setup({ - securityLicense: new SecurityLicenseService().setup({ license$ }).license, - authc: securityMock.createSetup().authc, - logoutUrl: '/some/logout/url', - }); - const coreStart = coreMock.createStart(); const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart }); const userMenuLinks$ = getUserMenuLinks$(); @@ -305,5 +290,79 @@ describe('SecurityNavControlService', () => { done(); }); }); + + it('should allow adding a custom profile link', () => { + const coreStart = coreMock.createStart(); + const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart }); + const userMenuLinks$ = getUserMenuLinks$(); + + addUserMenuLinks([ + { label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3 }, + { label: 'link1', href: 'path-to-link1', iconType: 'empty', order: 1, setAsProfile: true }, + ]); + + const onUserMenuLinksHandler = jest.fn(); + userMenuLinks$.subscribe(onUserMenuLinksHandler); + + expect(onUserMenuLinksHandler).toHaveBeenCalledTimes(1); + expect(onUserMenuLinksHandler).toHaveBeenCalledWith([ + { label: 'link1', href: 'path-to-link1', iconType: 'empty', order: 1, setAsProfile: true }, + { label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3 }, + ]); + }); + + it('should not allow adding more than one custom profile link', () => { + const coreStart = coreMock.createStart(); + const { getUserMenuLinks$, addUserMenuLinks } = navControlService.start({ core: coreStart }); + const userMenuLinks$ = getUserMenuLinks$(); + + expect(() => { + addUserMenuLinks([ + { + label: 'link3', + href: 'path-to-link3', + iconType: 'empty', + order: 3, + setAsProfile: true, + }, + { + label: 'link1', + href: 'path-to-link1', + iconType: 'empty', + order: 1, + setAsProfile: true, + }, + ]); + }).toThrowErrorMatchingInlineSnapshot( + `"Only one custom profile link can be passed at a time (found 2)"` + ); + + // Adding a single custom profile link. + addUserMenuLinks([ + { label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3, setAsProfile: true }, + ]); + + expect(() => { + addUserMenuLinks([ + { + label: 'link1', + href: 'path-to-link1', + iconType: 'empty', + order: 1, + setAsProfile: true, + }, + ]); + }).toThrowErrorMatchingInlineSnapshot( + `"Only one custom profile link can be set. A custom profile link named link3 (path-to-link3) already exists"` + ); + + const onUserMenuLinksHandler = jest.fn(); + userMenuLinks$.subscribe(onUserMenuLinksHandler); + + expect(onUserMenuLinksHandler).toHaveBeenCalledTimes(1); + expect(onUserMenuLinksHandler).toHaveBeenCalledWith([ + { label: 'link3', href: 'path-to-link3', iconType: 'empty', order: 3, setAsProfile: true }, + ]); + }); }); }); diff --git a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx index fc9ba262a2026..7f3d93099704a 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_service.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_service.tsx @@ -77,6 +77,23 @@ export class SecurityNavControlService { this.userMenuLinks$.pipe(map(this.sortUserMenuLinks), takeUntil(this.stop$)), addUserMenuLinks: (userMenuLinks: UserMenuLink[]) => { const currentLinks = this.userMenuLinks$.value; + const hasCustomProfileLink = currentLinks.find(({ setAsProfile }) => setAsProfile === true); + const passedCustomProfileLinkCount = userMenuLinks.filter( + ({ setAsProfile }) => setAsProfile === true + ).length; + + if (hasCustomProfileLink && passedCustomProfileLinkCount > 0) { + throw new Error( + `Only one custom profile link can be set. A custom profile link named ${hasCustomProfileLink.label} (${hasCustomProfileLink.href}) already exists` + ); + } + + if (passedCustomProfileLinkCount > 1) { + throw new Error( + `Only one custom profile link can be passed at a time (found ${passedCustomProfileLinkCount})` + ); + } + const newLinks = [...currentLinks, ...userMenuLinks]; this.userMenuLinks$.next(newLinks); }, diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index a578fb932068d..aaae0d4dc25ef 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -45,6 +45,7 @@ export { Type, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, + osType, osTypeArray, OsTypeArray, buildExceptionFilter, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 3a2170d126a24..b0ffcb8c5b5b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -33,7 +33,6 @@ import { import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; -import { osTypeArray, OsTypeArray } from '../../../../../common/shared_imports'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; import { ExceptionBuilderComponent } from '../builder'; @@ -50,6 +49,7 @@ import { defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, + retrieveAlertOsTypes, } from '../helpers'; import { ErrorInfo, ErrorCallout } from '../error_callout'; import { AlertData, ExceptionsBuilderExceptionItem } from '../types'; @@ -291,18 +291,6 @@ export const AddExceptionModal = memo(function AddExceptionModal({ [setShouldBulkCloseAlert] ); - const retrieveAlertOsTypes = useCallback((): OsTypeArray => { - const osDefaults: OsTypeArray = ['windows', 'macos']; - if (alertData != null) { - const osTypes = alertData.host && alertData.host.os && alertData.host.os.family; - if (osTypeArray.is(osTypes) && osTypes != null && osTypes.length > 0) { - return osTypes; - } - return osDefaults; - } - return osDefaults; - }, [alertData]); - const enrichExceptionItems = useCallback((): Array< ExceptionListItemSchema | CreateExceptionListItemSchema > => { @@ -312,11 +300,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ? enrichNewExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }]) : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { - const osTypes = retrieveAlertOsTypes(); + const osTypes = retrieveAlertOsTypes(alertData); enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; - }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); + }, [comment, exceptionItemsToAdd, exceptionListType, alertData]); const onAddExceptionConfirm = useCallback((): void => { if (addOrUpdateExceptionItems != null) { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index f21f189438890..3463f521655cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -29,6 +29,7 @@ import { defaultEndpointExceptionItems, getFileCodeSignature, getProcessCodeSignature, + retrieveAlertOsTypes, } from './helpers'; import { AlertData, EmptyEntry } from './types'; import { @@ -533,6 +534,25 @@ describe('Exception helpers', () => { }); }); + describe('#retrieveAlertOsTypes', () => { + test('it should retrieve os type if alert data is provided', () => { + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + host: { os: { family: 'windows' } }, + }; + const result = retrieveAlertOsTypes(alertDataMock); + const expected = ['windows']; + expect(result).toEqual(expected); + }); + + test('it should return default os types if alert data is not provided', () => { + const result = retrieveAlertOsTypes(); + const expected = ['windows', 'macos']; + expect(result).toEqual(expected); + }); + }); + describe('#entryHasListType', () => { test('it should return false with an empty array', () => { const payload: ExceptionListItemSchema[] = []; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 04502d1e16204..43c3b6c082f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -13,6 +13,7 @@ import uuid from 'uuid'; import * as i18n from './translations'; import { + AlertData, BuilderEntry, CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem, @@ -39,6 +40,7 @@ import { EntryNested, OsTypeArray, EntriesArray, + osType, } from '../../../shared_imports'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { validate } from '../../../../common/validate'; @@ -359,6 +361,17 @@ export const enrichExceptionItemsWithOS = ( }); }; +export const retrieveAlertOsTypes = (alertData?: AlertData): OsTypeArray => { + const osDefaults: OsTypeArray = ['windows', 'macos']; + if (alertData != null) { + const os = alertData.host && alertData.host.os && alertData.host.os.family; + if (os != null) { + return osType.is(os) ? [os] : osDefaults; + } + } + return osDefaults; +}; + /** * Returns given exceptionItems with all hash-related entries lowercased */ diff --git a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx deleted file mode 100644 index 65da5423d19e8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { TestProviders } from '../../mock'; - -import { ExternalLinkIcon } from '.'; - -describe('Duration', () => { - test('it renders expected icon type when the leftMargin prop is not specified', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().props().type).toEqual( - 'popout' - ); - }); - - test('it renders expected icon type when the leftMargin prop is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().props().type).toEqual( - 'popout' - ); - }); - - test('it applies a margin-left style when the leftMargin prop is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first()).toHaveStyleRule( - 'margin-left', - '5px' - ); - }); - - test('it does NOT apply a margin-left style when the leftMargin prop is false', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first()).not.toHaveStyleRule( - 'margin-left' - ); - }); - - test('it renders expected icon type when the leftMargin prop is false', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().props().type).toEqual( - 'popout' - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx deleted file mode 100644 index 825ee8a33a8a5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiIcon } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -const LinkIcon = styled(EuiIcon)` - position: relative; - top: -2px; -`; - -LinkIcon.displayName = 'LinkIcon'; - -const LinkIconWithMargin = styled(LinkIcon)` - margin-left: 5px; -`; - -LinkIconWithMargin.displayName = 'LinkIconWithMargin'; - -const color = 'subdued'; -const iconSize = 's'; -const iconType = 'popout'; - -/** - * Renders an icon that indicates following the hyperlink will navigate to - * content external to the app - */ -export const ExternalLinkIcon = React.memo<{ - leftMargin?: boolean; -}>(({ leftMargin = true }) => - leftMargin ? ( - - ) : ( - - ) -); - -ExternalLinkIcon.displayName = 'ExternalLinkIcon'; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 864e55b3e4a45..cd19eb5a27d7b 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { mount, shallow, ShallowWrapper } from 'enzyme'; +import { mount, shallow, ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { removeExternalLinkText } from '../../../../common/test_utils'; import { mountWithIntl } from '@kbn/test/jest'; @@ -121,11 +121,11 @@ describe('Custom Links', () => { describe('External Link', () => { const mockLink = 'https://www.virustotal.com/gui/search/'; const mockLinkName = 'Link'; - let wrapper: ShallowWrapper; + let wrapper: ReactWrapper | ShallowWrapper; describe('render', () => { beforeAll(() => { - wrapper = shallow( + wrapper = mount( {mockLinkName} @@ -137,11 +137,13 @@ describe('Custom Links', () => { }); test('it renders ExternalLinkIcon', () => { - expect(wrapper.find('[data-test-subj="externalLinkIcon"]').exists()).toBeTruthy(); + expect(wrapper.find('span [data-euiicon-type="popout"]').length).toBe(1); }); test('it renders correct url', () => { - expect(wrapper.find('[data-test-subj="externalLink"]').prop('href')).toEqual(mockLink); + expect(wrapper.find('[data-test-subj="externalLink"]').first().prop('href')).toEqual( + mockLink + ); }); test('it renders comma if id is given', () => { @@ -435,14 +437,14 @@ describe('Custom Links', () => { test('it renders correct number of external icons by default', () => { const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(5); + expect(wrapper.find('span [data-euiicon-type="popout"]')).toHaveLength(5); }); test('it renders correct number of external icons', () => { const wrapper = mountWithIntl( ); - expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(1); + expect(wrapper.find('span [data-euiicon-type="popout"]')).toHaveLength(1); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 8e2f57a1a597c..a02cc8bf76bcc 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -39,7 +39,6 @@ import { } from '../../../../common/search_strategy/security_solution/network'; import { useUiSetting$, useKibana } from '../../lib/kibana'; import { isUrlInvalid } from '../../utils/validators'; -import { ExternalLinkIcon } from '../external_link_icon'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; @@ -54,6 +53,13 @@ export const LinkAnchor: React.FC = ({ children, ...props }) => ( {children} ); +export const PortContainer = styled.div` + & svg { + position: relative; + top: -1px; + } +`; + // Internal Links const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; @@ -112,11 +118,12 @@ export const ExternalLink = React.memo<{ const inAllowlist = allowedUrlSchemes.some((scheme) => url.indexOf(scheme) === 0); return url && inAllowlist && !isUrlInvalid(url) && children ? ( - - {children} - + <> + + {children} + {!isNil(idx) && idx < lastIndexToShow && } - + ) : null; } @@ -229,15 +236,17 @@ export const PortOrServiceNameLink = React.memo<{ children?: React.ReactNode; portOrServiceName: number | string; }>(({ children, portOrServiceName }) => ( - - {children ? children : portOrServiceName} - + + + {children ? children : portOrServiceName} + + )); PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; diff --git a/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap index ecdaee0ab2d93..f84e858d20573 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap @@ -11,6 +11,5 @@ exports[`Port renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index 47dfffefe091c..9c7a0833b24bb 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -60,13 +60,13 @@ describe('Port', () => { ); }); - test('it renders an external link', () => { + test('it renders only one external link icon', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().exists()).toBe(true); + expect(wrapper.find('span [data-euiicon-type="popout"]').length).toBe(1); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.tsx index 0744ca175aa38..8ee1616d4c77b 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { DefaultDraggable } from '../../../common/components/draggables'; import { getEmptyValue } from '../../../common/components/empty_value'; -import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; import { PortOrServiceNameLink } from '../../../common/components/links'; export const CLIENT_PORT_FIELD_NAME = 'client.port'; @@ -40,7 +39,6 @@ export const Port = React.memo<{ value={value} > - )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx index d850204284bd0..29775067478a5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../../../common/components/draggables'; -import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; import { CertificateFingerprintLink } from '../../../common/components/links'; import * as i18n from './translations'; @@ -61,7 +60,6 @@ export const CertificateFingerprint = React.memo<{ {certificateType === 'client' ? i18n.CLIENT_CERT : i18n.SERVER_CERT} - ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx index df8c68df483c5..d73130417566f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx @@ -9,7 +9,6 @@ import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../../../common/components/draggables'; -import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; import { Ja3FingerprintLink } from '../../../common/components/links'; import * as i18n from './translations'; @@ -45,7 +44,6 @@ export const Ja3Fingerprint = React.memo<{ {i18n.JA3_FINGERPRINT_LABEL} - )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx index 65974a10c49c2..283a239acad24 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -7,7 +7,6 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { ExternalLinkIcon } from '../../../../common/components/external_link_icon'; import { RowRendererId } from '../../../../../common/types/timeline'; import { @@ -37,7 +36,6 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => data-test-subj="externalLink" > {children} -
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx index 0bbd86479c226..96e6b916a3e11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx @@ -9,7 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { getLinksFromSignature } from './suricata_links'; const LinkEuiFlexItem = styled(EuiFlexItem)` @@ -27,7 +26,6 @@ export const SuricataRefs = React.memo<{ signatureId: number }>(({ signatureId } {link} - ))} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 905078f676eef..8b9afa7cacc0c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -162,19 +162,20 @@ export class Plugin implements IPlugin => Promise.resolve(config), + }; + initUsageCollectors({ core, + endpointAppContext: endpointContext, kibanaIndex: globalConfig.kibana.index, ml: plugins.ml, usageCollection: plugins.usageCollection, }); - const endpointContext: EndpointAppContext = { - logFactory: this.context.logger, - service: this.endpointAppContextService, - config: (): Promise => Promise.resolve(config), - }; - const router = core.http.createRouter(); core.http.registerRouteHandlerContext( APP_ID, diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 981101bf733c7..53fa1a1571835 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -31,6 +31,7 @@ export async function getInternalSavedObjectsClient(core: CoreSetup) { export const registerCollector: RegisterCollector = ({ core, + endpointAppContext, kibanaIndex, ml, usageCollection, @@ -138,7 +139,7 @@ export const registerCollector: RegisterCollector = ({ const [detections, detectionMetrics, endpoints] = await Promise.allSettled([ fetchDetectionsUsage(kibanaIndex, esClient, ml, savedObjectsClient), fetchDetectionsMetrics(ml, savedObjectsClient), - getEndpointTelemetryFromFleet(internalSavedObjectsClient), + getEndpointTelemetryFromFleet(savedObjectsClient, endpointAppContext, esClient), ]); return { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index 2c6a1bb69cf27..caa4549fbf31d 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -7,10 +7,7 @@ import { SavedObjectsFindResponse } from 'src/core/server'; import { AgentEventSOAttributes } from './../../../../fleet/common/types/models/agent'; -import { - AGENT_SAVED_OBJECT_TYPE, - AGENT_EVENT_SAVED_OBJECT_TYPE, -} from '../../../../fleet/common/constants/agent'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../../fleet/common/constants/agent'; import { Agent } from '../../../../fleet/common'; import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; @@ -36,84 +33,68 @@ export const MockOSFullName = 'somePlatformFullName'; export const mockFleetObjectsResponse = ( hasDuplicates = true, lastCheckIn = new Date().toISOString() -): SavedObjectsFindResponse => ({ +): { agents: Agent[]; total: number; page: number; perPage: number } | undefined => ({ page: 1, - per_page: 20, + perPage: 20, total: 1, - saved_objects: [ + agents: [ { - type: AGENT_SAVED_OBJECT_TYPE, + active: true, id: testAgentId, - attributes: { - active: true, - id: testAgentId, - policy_id: 'randoAgentPolicyId', - type: 'PERMANENT', - user_provided_metadata: {}, - enrolled_at: lastCheckIn, - current_error_events: [], - local_metadata: { - elastic: { - agent: { - id: testAgentId, - }, - }, - host: { - hostname: testHostName, - name: testHostName, - id: testHostId, - }, - os: { - platform: MockOSPlatform, - version: MockOSVersion, - name: MockOSName, - full: MockOSFullName, + policy_id: 'randoAgentPolicyId', + type: 'PERMANENT', + user_provided_metadata: {}, + enrolled_at: lastCheckIn, + current_error_events: [], + local_metadata: { + elastic: { + agent: { + id: testAgentId, }, }, - packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], - last_checkin: lastCheckIn, + host: { + hostname: testHostName, + name: testHostName, + id: testHostId, + }, + os: { + platform: MockOSPlatform, + version: MockOSVersion, + name: MockOSName, + full: MockOSFullName, + }, }, - references: [], - updated_at: lastCheckIn, - version: 'WzI4MSwxXQ==', - score: 0, + packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], + last_checkin: lastCheckIn, }, { - type: AGENT_SAVED_OBJECT_TYPE, - id: testAgentId, - attributes: { - active: true, - id: 'oldTestAgentId', - policy_id: 'randoAgentPolicyId', - type: 'PERMANENT', - user_provided_metadata: {}, - enrolled_at: lastCheckIn, - current_error_events: [], - local_metadata: { - elastic: { - agent: { - id: 'oldTestAgentId', - }, - }, - host: { - hostname: hasDuplicates ? testHostName : 'oldRandoHostName', - name: hasDuplicates ? testHostName : 'oldRandoHostName', - id: hasDuplicates ? testHostId : 'oldRandoHostId', - }, - os: { - platform: MockOSPlatform, - version: MockOSVersion, - name: MockOSName, - full: MockOSFullName, + active: true, + id: 'oldTestAgentId', + policy_id: 'randoAgentPolicyId', + type: 'PERMANENT', + user_provided_metadata: {}, + enrolled_at: lastCheckIn, + current_error_events: [], + local_metadata: { + elastic: { + agent: { + id: 'oldTestAgentId', }, }, - packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], - last_checkin: lastCheckIn, + host: { + hostname: hasDuplicates ? testHostName : 'oldRandoHostName', + name: hasDuplicates ? testHostName : 'oldRandoHostName', + id: hasDuplicates ? testHostId : 'oldRandoHostId', + }, + os: { + platform: MockOSPlatform, + version: MockOSVersion, + name: MockOSName, + full: MockOSFullName, + }, }, - references: [], - updated_at: lastCheckIn, - version: 'WzI4MSwxXQ==', - score: 0, + packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], + last_checkin: lastCheckIn, }, ], }); diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts index aaf85a0201478..1541cb128f60c 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { mockFleetObjectsResponse, mockFleetEventsObjectsResponse, @@ -13,23 +13,34 @@ import { MockOSPlatform, MockOSVersion, } from './endpoint.mocks'; -import { ISavedObjectsRepository, SavedObjectsFindResponse } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; import { AgentEventSOAttributes } from '../../../../fleet/common/types/models/agent'; import { Agent } from '../../../../fleet/common'; import * as endpointTelemetry from './index'; import * as fleetSavedObjects from './fleet_saved_objects'; +import { createMockEndpointAppContext } from '../../endpoint/mocks'; +import { EndpointAppContext } from '../../endpoint/types'; describe('test security solution endpoint telemetry', () => { - let mockSavedObjectsRepository: jest.Mocked; - let getFleetSavedObjectsMetadataSpy: jest.SpyInstance>>; + let mockSavedObjectsClient: jest.Mocked; + let mockEndpointAppContext: EndpointAppContext; + let mockEsClient: ReturnType; + let getEndpointIntegratedFleetMetadataSpy: jest.SpyInstance< + Promise<{ agents: Agent[]; total: number; page: number; perPage: number } | undefined> + >; let getLatestFleetEndpointEventSpy: jest.SpyInstance< Promise> >; beforeAll(() => { getLatestFleetEndpointEventSpy = jest.spyOn(fleetSavedObjects, 'getLatestFleetEndpointEvent'); - getFleetSavedObjectsMetadataSpy = jest.spyOn(fleetSavedObjects, 'getFleetSavedObjectsMetadata'); - mockSavedObjectsRepository = savedObjectsRepositoryMock.create(); + getEndpointIntegratedFleetMetadataSpy = jest.spyOn( + fleetSavedObjects, + 'getEndpointIntegratedFleetMetadata' + ); + mockSavedObjectsClient = savedObjectsClientMock.create(); + mockEndpointAppContext = createMockEndpointAppContext(); + mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); }); afterAll(() => { @@ -55,28 +66,32 @@ describe('test security solution endpoint telemetry', () => { describe('when a request for endpoint agents fails', () => { it('should return an empty object', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.reject(Error('No agents for you')) ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); - expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); + expect(getEndpointIntegratedFleetMetadataSpy).toHaveBeenCalled(); expect(endpointUsage).toEqual({}); }); }); describe('when an agent has not been installed', () => { it('should return the default shape if no agents are found', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => - Promise.resolve({ saved_objects: [], total: 0, per_page: 0, page: 0 }) + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => + Promise.resolve({ agents: [], total: 0, perPage: 0, page: 0 }) ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); - expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); + expect(getEndpointIntegratedFleetMetadataSpy).toHaveBeenCalled(); expect(endpointUsage).toEqual({ total_installed: 0, active_within_last_24_hours: 0, @@ -95,7 +110,7 @@ describe('test security solution endpoint telemetry', () => { describe('when agent(s) have been installed', () => { describe('when a request for events has failed', () => { it('should show only one endpoint installed but it is inactive', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -103,7 +118,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -129,7 +146,7 @@ describe('test security solution endpoint telemetry', () => { describe('when a request for events is successful', () => { it('should show one endpoint installed but endpoint has failed to run', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -137,7 +154,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -161,7 +180,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should show two endpoints installed but both endpoints have failed to run', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse(false)) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -169,7 +188,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 2, @@ -197,7 +218,7 @@ describe('test security solution endpoint telemetry', () => { twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); const twoDaysAgoISOString = twoDaysAgo.toISOString(); - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse(false, twoDaysAgoISOString)) ); getLatestFleetEndpointEventSpy.mockImplementation( @@ -205,7 +226,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 2, @@ -229,7 +252,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should show one endpoint installed and endpoint is running', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -237,7 +260,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -262,7 +287,7 @@ describe('test security solution endpoint telemetry', () => { describe('malware policy', () => { it('should have failed to enable', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -272,7 +297,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -296,7 +323,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should be enabled successfully', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -304,7 +331,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -328,7 +357,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should be disabled successfully', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -338,7 +367,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index e96ce0b2dda76..7e3620ec0ae04 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -5,38 +5,36 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import { AgentService } from '../../../../fleet/server'; import { AgentEventSOAttributes } from './../../../../fleet/common/types/models/agent'; -import { - AGENT_SAVED_OBJECT_TYPE, - AGENT_EVENT_SAVED_OBJECT_TYPE, -} from './../../../../fleet/common/constants/agent'; -import { Agent, defaultPackages as FleetDefaultPackages } from '../../../../fleet/common'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from './../../../../fleet/common/constants/agent'; +import { defaultPackages as FleetDefaultPackages } from '../../../../fleet/common'; export const FLEET_ENDPOINT_PACKAGE_CONSTANT = FleetDefaultPackages.Endpoint; -export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObjectsRepository) => - savedObjectsClient.find({ - // Get up to 10000 agents with endpoint installed - type: AGENT_SAVED_OBJECT_TYPE, - fields: [ - 'packages', - 'last_checkin', - 'local_metadata.agent.id', - 'local_metadata.host.id', - 'local_metadata.host.name', - 'local_metadata.host.hostname', - 'local_metadata.elastic.agent.id', - 'local_metadata.os', - ], - filter: `${AGENT_SAVED_OBJECT_TYPE}.attributes.packages: ${FLEET_ENDPOINT_PACKAGE_CONSTANT}`, +export const getEndpointIntegratedFleetMetadata = async ( + agentService: AgentService | undefined, + esClient: ElasticsearchClient +) => { + return agentService?.listAgents(esClient, { + kuery: `(packages : ${FLEET_ENDPOINT_PACKAGE_CONSTANT})`, perPage: 10000, + showInactive: false, sortField: 'enrolled_at', sortOrder: 'desc', }); +}; + +/* + TODO: AS OF 7.13, this access will no longer work due to the enabling of fleet server. An alternative route will have + to be discussed to retrieve the policy data we need, as well as when the endpoint was last active, which is obtained + via the last endpoint 'check in' event that was sent to fleet. Also, the only policy currently tracked is `malware`, + but the hope is to add more, so a better/more scalable solution would be desirable. +*/ export const getLatestFleetEndpointEvent = async ( - savedObjectsClient: ISavedObjectsRepository, + savedObjectsClient: SavedObjectsClientContract, agentId: string ) => savedObjectsClient.find({ diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index 48cb50a493edf..94ff168ffffc8 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -6,11 +6,15 @@ */ import { cloneDeep } from 'lodash'; -import { ISavedObjectsRepository } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { SavedObject } from './../../../../../../src/core/types/saved_objects'; import { Agent, NewAgentEvent } from './../../../../fleet/common/types/models/agent'; import { AgentMetadata } from '../../../../fleet/common/types/models/agent'; -import { getFleetSavedObjectsMetadata, getLatestFleetEndpointEvent } from './fleet_saved_objects'; +import { + getEndpointIntegratedFleetMetadata, + getLatestFleetEndpointEvent, +} from './fleet_saved_objects'; +import { EndpointAppContext } from '../../endpoint/types'; export interface AgentOSMetadataTelemetry { full_name: string; @@ -108,7 +112,7 @@ export const updateEndpointOSTelemetry = ( * the same time span. */ export const updateEndpointDailyActiveCount = ( - latestEndpointEvent: SavedObject, + latestEndpointEvent: SavedObject, // TODO: This information will be lost in 7.13, need to find an alternative route. lastAgentCheckin: Agent['last_checkin'], currentCount: number ) => { @@ -193,19 +197,22 @@ export const updateEndpointPolicyTelemetry = ( }; /** - * @description This aggregates the telemetry details from the two fleet savedObject sources, `fleet-agents` and `fleet-agent-events` to populate + * @description This aggregates the telemetry details from the fleet agent service `listAgents` and the fleet saved object `fleet-agent-events` to populate * the telemetry details for endpoint. Since we cannot access our own indices due to `kibana_system` not having access, this is the best alternative. * Once the data is requested, we iterate over all agents with endpoints registered, and then request the events for each active agent (within last 24 hours) * to confirm whether or not the endpoint is still active */ export const getEndpointTelemetryFromFleet = async ( - soClient: ISavedObjectsRepository + soClient: SavedObjectsClientContract, + endpointAppContext: EndpointAppContext, + esClient: ElasticsearchClient ): Promise => { // Retrieve every agent (max 10000) that references the endpoint as an installed package. It will not be listed if it was never installed let endpointAgents; + const agentService = endpointAppContext.service.getAgentService(); try { - const response = await getFleetSavedObjectsMetadata(soClient); - endpointAgents = response.saved_objects; + const response = await getEndpointIntegratedFleetMetadata(agentService, esClient); + endpointAgents = response?.agents ?? []; } catch (error) { // Better to provide an empty object rather than default telemetry as this better informs us of an error return {}; @@ -225,8 +232,7 @@ export const getEndpointTelemetryFromFleet = async ( for (let i = 0; i < endpointAgentsCount; i += 1) { try { - const { attributes: metadataAttributes } = endpointAgents[i]; - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = endpointAgents[i]; const { host, os, elastic } = localMetadata as AgentLocalMetadata; // Although not perfect, the goal is to dedupe hosts to get the most recent data for a host diff --git a/x-pack/plugins/security_solution/server/usage/types.ts b/x-pack/plugins/security_solution/server/usage/types.ts index 562b6a5278f64..c06c8a4722cd7 100644 --- a/x-pack/plugins/security_solution/server/usage/types.ts +++ b/x-pack/plugins/security_solution/server/usage/types.ts @@ -6,9 +6,11 @@ */ import { CoreSetup } from 'src/core/server'; +import { EndpointAppContext } from '../endpoint/types'; import { SetupPlugins } from '../plugin'; -export type CollectorDependencies = { kibanaIndex: string; core: CoreSetup } & Pick< - SetupPlugins, - 'ml' | 'usageCollection' ->; +export type CollectorDependencies = { + kibanaIndex: string; + core: CoreSetup; + endpointAppContext: EndpointAppContext; +} & Pick; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index d844651d0b8aa..86e07fa64af66 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -5,10 +5,19 @@ * 2.0. */ +import uuid from 'uuid'; import type { Writable } from '@kbn/utility-types'; +import { AlertServices } from '../../../../alerting/server'; +import { + AlertServicesMock, + alertsMock, + AlertInstanceMock, +} from '../../../../alerting/server/mocks'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; -import { getAlertType } from './alert_type'; -import { EsQueryAlertParams } from './alert_type_params'; +import { getAlertType, ConditionMetAlertInstanceId, ActionGroupId } from './alert_type'; +import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; +import { ActionContext } from './action_context'; +import { ESSearchResponse, ESSearchRequest } from '../../../../../typings/elasticsearch'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); @@ -108,4 +117,476 @@ describe('alertType', () => { `"[threshold]: must have two elements for the \\"between\\" comparator"` ); }); + + it('alert executor handles no documentes returned by ES', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: 'between', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const searchResult: ESSearchResponse = generateResults([]); + alertServices.callCluster.mockResolvedValueOnce(searchResult); + + const result = await alertType.executor({ + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + latestTimestamp: undefined, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }); + + expect(alertServices.alertInstanceFactory).not.toHaveBeenCalled(); + + expect(result).toMatchInlineSnapshot(` + Object { + "latestTimestamp": undefined, + } + `); + }); + + it('alert executor returns the latestTimestamp of the newest detected document', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const newestDocumentTimestamp = Date.now(); + + const searchResult: ESSearchResponse = generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + { + 'time-field': newestDocumentTimestamp - 1000, + }, + { + 'time-field': newestDocumentTimestamp - 2000, + }, + ]); + alertServices.callCluster.mockResolvedValueOnce(searchResult); + + const result = await alertType.executor({ + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + latestTimestamp: undefined, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }); + + expect(alertServices.alertInstanceFactory).toHaveBeenCalledWith(ConditionMetAlertInstanceId); + const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const previousTimestamp = Date.now(); + const newestDocumentTimestamp = previousTimestamp + 1000; + + alertServices.callCluster.mockResolvedValueOnce( + generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + ]) + ); + + const executorOptions = { + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }; + const result = await alertType.executor({ + ...executorOptions, + state: { + // @ts-expect-error previousTimestamp is numeric, but should be string (this was a bug prior to v7.12.1) + latestTimestamp: previousTimestamp, + }, + }); + + const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward + latestTimestamp: new Date(previousTimestamp).toISOString(), + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.callCluster.mockResolvedValueOnce( + generateResults([ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ]) + ); + + const result = await alertType.executor({ + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + // inaalid legacy `latestTimestamp` + latestTimestamp: 'FaslK3QBySSL_rrj9zM5', + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }); + + const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor carries over the queried latestTimestamp in the alert state', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.callCluster.mockResolvedValueOnce( + generateResults([ + { + 'time-field': oldestDocumentTimestamp, + }, + ]) + ); + + const executorOptions = { + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + latestTimestamp: undefined, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }; + const result = await alertType.executor(executorOptions); + + const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + }); + + const newestDocumentTimestamp = oldestDocumentTimestamp + 5000; + alertServices.callCluster.mockResolvedValueOnce( + generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + { + 'time-field': newestDocumentTimestamp - 1000, + }, + ]) + ); + + const secondResult = await alertType.executor({ + ...executorOptions, + state: result as EsQueryAlertState, + }); + const existingInstance: AlertInstanceMock = + alertServices.alertInstanceFactory.mock.results[1].value; + expect(existingInstance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(secondResult).toMatchObject({ + latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor ignores tie breaker sort values', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.callCluster.mockResolvedValueOnce( + generateResults( + [ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ], + true + ) + ); + + const result = await alertType.executor({ + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + latestTimestamp: undefined, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }); + + const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor ignores results with no sort values', async () => { + const params: EsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.callCluster.mockResolvedValueOnce( + generateResults( + [ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ], + true, + true + ) + ); + + const result = await alertType.executor({ + alertId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: (alertServices as unknown) as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params, + state: { + latestTimestamp: undefined, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + }); + + const instance: AlertInstanceMock = alertServices.alertInstanceFactory.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp - 1000).toISOString(), + }); + }); }); + +function generateResults( + docs: Array<{ 'time-field': unknown; [key: string]: unknown }>, + includeTieBreaker: boolean = false, + skipSortOnFirst: boolean = false +): ESSearchResponse { + const hits = docs.map((doc, index) => ({ + _index: 'foo', + _type: '_doc', + _id: `${index}`, + _score: 0, + ...(skipSortOnFirst && index === 0 + ? {} + : { + sort: (includeTieBreaker + ? ['FaslK3QBySSL_rrj9zM5', doc['time-field']] + : [doc['time-field']]) as string[], + }), + _source: doc, + })); + return { + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: { + value: docs.length, + relation: 'eq', + }, + max_score: 100, + hits, + }, + }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index bad25a0d1d09c..74af8b0038a3a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -23,8 +23,8 @@ import { ESSearchHit } from '../../../../../typings/elasticsearch'; export const ES_QUERY_ID = '.es-query'; -const ActionGroupId = 'query matched'; -const ConditionMetAlertInstanceId = 'query matched'; +export const ActionGroupId = 'query matched'; +export const ConditionMetAlertInstanceId = 'query matched'; export function getAlertType( logger: Logger @@ -173,7 +173,7 @@ export function getAlertType( // of the alert, the latestTimestamp will be used to gate the query in order to // avoid counting a document multiple times. - let timestamp: string | undefined = previousTimestamp; + let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); const filter = timestamp ? { bool: { @@ -187,7 +187,7 @@ export function getAlertType( filter: [ { range: { - [params.timeField]: { lte: new Date(timestamp).toISOString() }, + [params.timeField]: { lte: timestamp }, }, }, ], @@ -251,12 +251,11 @@ export function getAlertType( .scheduleActions(ActionGroupId, actionContext); // update the timestamp based on the current search results - const firstHitWithSort = searchResult.hits.hits.find( - (hit: ESSearchHit) => hit.sort != null + const firstValidTimefieldSort = getValidTimefieldSort( + searchResult.hits.hits.find((hit: ESSearchHit) => getValidTimefieldSort(hit.sort))?.sort ); - const lastTimestamp = firstHitWithSort?.sort; - if (lastTimestamp != null && lastTimestamp.length > 0) { - timestamp = lastTimestamp[0]; + if (firstValidTimefieldSort) { + timestamp = firstValidTimefieldSort; } } } @@ -267,6 +266,21 @@ export function getAlertType( } } +function getValidTimefieldSort(sortValues: Array = []): undefined | string { + for (const sortValue of sortValues) { + const sortDate = tryToParseAsDate(sortValue); + if (sortDate) { + return sortDate; + } + } +} +function tryToParseAsDate(sortValue?: string | number): undefined | string { + const sortDate = typeof sortValue === 'string' ? Date.parse(sortValue) : sortValue; + if (sortDate && !isNaN(sortDate)) { + return new Date(sortDate).toISOString(); + } +} + function getInvalidComparatorError(comparator: string) { return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { defaultMessage: 'invalid thresholdComparator specified: {comparator}', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index bcb07c8069ab2..fa1a42b94f0b7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -105,7 +105,7 @@ export const TransformManagement: FC = () => { diff --git a/x-pack/plugins/transform/public/register_feature.ts b/x-pack/plugins/transform/public/register_feature.ts index 2702ef9f616d6..d105424052411 100644 --- a/x-pack/plugins/transform/public/register_feature.ts +++ b/x-pack/plugins/transform/public/register_feature.ts @@ -20,7 +20,7 @@ export const registerFeature = (home: HomePublicPluginSetup) => { }), description: i18n.translate('xpack.transform.transformsDescription', { defaultMessage: - 'Use transforms to pivot existing Elasticsearch indices into summarized or entity-centric indices.', + 'Use transforms to pivot existing Elasticsearch indices into summarized entity-centric indices or to create an indexed view of the latest documents for fast access.', }), icon: 'managementApp', // there is currently no Transforms icon, so using the general management app icon path: '/app/management/data/transform', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9e4ec1dff0e30..8eec34c137ecb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10046,7 +10046,6 @@ "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "コールドティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。安価なハードウェアのコールドフェーズにデータを格納します。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", - "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "レプリカの数", "xpack.indexLifecycleMgmt.common.dataTier.title": "データ割り当て", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除", @@ -10061,16 +10060,10 @@ "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "コールドフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "データをコールドティアに移動します。これは、検索パフォーマンスよりもコスト削減を優先するように最適化されています。通常、コールドフェーズではデータが読み取り専用です。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "コールドフェーズ", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "インデックスを読み取り専用にし、メモリー消費量を最小化します。", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "凍結", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "ノード属性に基づいてデータを移動します。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "カスタム", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "コールドティアのノードにデータを移動します。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "コールドノードを使用 (推奨) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "コールドフェーズにデータを移動しないでください。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "オフ", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "ノード属性に基づいてデータを移動します。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "カスタム", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "ウォームティアのノードにデータを移動します。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "ウォームノードを使用 (推奨) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "ウォームフェーズにデータを移動しないでください。", @@ -10183,7 +10176,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "ライフサイクルポリシー {lifecycleName} の保存中にエラーが発生しました", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "検索可能なスナップショットがホットフェーズで有効な場合には、強制、マージ、縮小、凍結、コールドフェーズの検索可能なスナップショットは許可されません。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "選択したリポジトリで管理されたインデックスのスナップショットを作成し、検索可能なスナップショットとしてマウントします。{learnMoreLink}", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "検索可能なスナップショットリポジドリ", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "検索可能なスナップショットを作成するには、エンタープライズライセンスが必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です", @@ -10349,7 +10341,6 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "このポリシーはウォームフェーズのデータを{tier}ティアノードに移動します。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", - "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数", "xpack.infra.alerting.alertDropdownTitle": "アラート", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", @@ -11859,7 +11850,6 @@ "xpack.lens.configure.invalidConfigTooltipClick": "詳細はクリックしてください。", "xpack.lens.customBucketContainer.dragToReorder": "ドラッグして並べ替え", "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える", - "xpack.lens.datatable.breakdown": "内訳の基準", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー", "xpack.lens.datatable.label": "データテーブル", @@ -17956,7 +17946,6 @@ "xpack.security.management.users.usersTitle": "ユーザー", "xpack.security.management.usersTitle": "ユーザー", "xpack.security.navControlComponent.accountMenuAriaLabel": "アカウントメニュー", - "xpack.security.navControlComponent.editProfileLinkText": "プロフィール", "xpack.security.navControlComponent.loginLinkText": "ログイン", "xpack.security.navControlComponent.logoutLinkText": "ログアウト", "xpack.security.overwrittenSession.continueAsUserText": "{username} として続行", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e1f700ae2a556..5ac0bd7d571fd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10173,7 +10173,6 @@ "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到冷层的节点", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。将处于冷阶段的数据存储在成本较低的硬件上。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", - "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目", "xpack.indexLifecycleMgmt.common.dataTier.title": "数据分配", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除", @@ -10188,16 +10187,10 @@ "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "激活冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "将数据移到经过优化后节省了成本但牺牲了搜索性能的冷层。数据在冷阶段通常为只读。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "冷阶段", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "使索引只读,并最大限度减小其内存占用。", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "冻结", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "根据节点属性移动数据。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "定制", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "将数据移到冷层中的节点。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "使用冷节点 (建议) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "不要移动冷阶段的数据。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "关闭", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "根据节点属性移动数据。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "定制", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "将数据移到温层中的节点。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "使用温节点 (建议) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "不要移动温阶段的数据。", @@ -10310,7 +10303,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "保存生命周期策略 {lifecycleName} 时出错", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "在热阶段启用可搜索快照时,不允许强制合并、缩小、冻结可搜索快照以及将其置入冷阶段。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "在所选存储库中拍取受管索引的快照,并将其安装为可搜索快照。{learnMoreLink}", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "可搜索快照存储库", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "要创建可搜索快照,需要企业许可证。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证", @@ -10480,7 +10472,6 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "此策略会改为将温阶段的数据移到{tier}层节点。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", - "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目", "xpack.infra.alerting.alertDropdownTitle": "告警", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", @@ -12016,7 +12007,6 @@ "xpack.lens.configure.invalidConfigTooltipClick": "单击了解更多详情。", "xpack.lens.customBucketContainer.dragToReorder": "拖动以重新排序", "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源", - "xpack.lens.datatable.breakdown": "细分方式", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器", "xpack.lens.datatable.label": "数据表", @@ -18206,7 +18196,6 @@ "xpack.security.management.users.usersTitle": "用户", "xpack.security.management.usersTitle": "用户", "xpack.security.navControlComponent.accountMenuAriaLabel": "帐户菜单", - "xpack.security.navControlComponent.editProfileLinkText": "配置文件", "xpack.security.navControlComponent.loginLinkText": "登录", "xpack.security.navControlComponent.logoutLinkText": "注销", "xpack.security.overwrittenSession.continueAsUserText": "作为 {username} 继续", diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js index ec3f5fe6fe349..e61470cc2cc84 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js @@ -75,8 +75,16 @@ export const getPolicyPayload = (name) => ({ }, }, }, + frozen: { + min_age: '20d', + actions: { + searchable_snapshot: { + snapshot_repository: 'backing_repo', + }, + }, + }, delete: { - min_age: '10d', + min_age: '30d', actions: { wait_for_snapshot: { policy: 'policy', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts index a319c30fa20de..919be0fcf311c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts @@ -21,7 +21,8 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('create_index', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/94367 + describe.skip('create_index', () => { afterEach(async () => { await deleteSignalsIndex(supertest); }); 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 59526fd5abb8f..a7925fa756693 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 @@ -5,6 +5,7 @@ * 2.0. */ +import { isEqual } from 'lodash'; import expect from '@kbn/expect'; import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; @@ -27,6 +28,18 @@ 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'; +const format = (value: unknown): string => JSON.stringify(value, null, 2); + +// Asserts that each expected value is included in the subject, independent of +// ordering. Uses _.isEqual for value comparison. +const assertContains = (subject: unknown[], expected: unknown[]) => + expected.forEach((expectedValue) => + expect(subject.some((value) => isEqual(value, expectedValue))).to.eql( + true, + `expected ${format(subject)} to contain ${format(expectedValue)}` + ) + ); + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); @@ -35,8 +48,7 @@ export default ({ getService }: FtrProviderContext) => { /** * Specific api integration tests for threat matching rule type */ - // FLAKY: https://github.com/elastic/kibana/issues/93152 - describe.skip('create_threat_matching', () => { + describe('create_threat_matching', () => { describe('validation errors', () => { it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { const { body } = await supertest @@ -383,40 +395,37 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(1); const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source.threat); - expect(threats).to.eql([ + const [threat] = hits.map((hit) => hit._source.threat) as Array<{ indicator: unknown[] }>; + + assertContains(threat.indicator, [ { - indicator: [ - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'ip', - }, - provider: 'other_provider', - type: 'ip', - }, - ], + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'ip', + }, + provider: 'other_provider', + type: 'ip', }, ]); }); @@ -466,61 +475,57 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(1); const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source.threat); + const [threat] = hits.map((hit) => hit._source.threat) as Array<{ indicator: unknown[] }>; - expect(threats).to.eql([ + assertContains(threat.indicator, [ { - indicator: [ - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - // We do not merge matched indicators during enrichment, so in - // certain circumstances a given indicator document could appear - // multiple times in an enriched alert (albeit with different - // threat.indicator.matched data). That's the case with the - // first and third indicators matched, here. - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'ip', - }, - provider: 'other_provider', - type: 'ip', - }, - ], + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + // We do not merge matched indicators during enrichment, so in + // certain circumstances a given indicator document could appear + // multiple times in an enriched alert (albeit with different + // threat.indicator.matched data). That's the case with the + // first and third indicators matched, here. + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'ip', + }, + provider: 'other_provider', + type: 'ip', }, ]); }); @@ -575,81 +580,77 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(2); const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source.threat); - expect(threats).to.eql([ + const threats = hits.map((hit) => hit._source.threat) as Array<{ indicator: unknown[] }>; + + assertContains(threats[0].indicator, [ { - indicator: [ - { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: 'url', - }, - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - ], + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, }, + ]); + + assertContains(threats[1].indicator, [ { - indicator: [ - { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: 'url', - }, - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - ], + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts index d54b4525459e8..cdb5035c06155 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts @@ -35,7 +35,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('deleting signals migrations', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/94367 + describe.skip('deleting signals migrations', () => { let outdatedSignalsIndexName: string; let createdMigration: CreateResponse; let finalizedMigration: FinalizeResponse; 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 0aac596cc3adb..89228fa4f239d 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,7 +47,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('Finalizing signals migrations', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/94367 + describe.skip('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index 5e4557057212f..519d33a947888 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -30,32 +30,32 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.dragFieldToDimensionTrigger( 'clientip', - 'lnsDatatable_column > lns-dimensionTrigger' + 'lnsDatatable_rows > lns-dimensionTrigger' ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows')).to.eql( 'Top values of clientip' ); await PageObjects.lens.dragFieldToDimensionTrigger( 'bytes', - 'lnsDatatable_column > lns-empty-dimension' + 'lnsDatatable_rows > lns-empty-dimension' ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows', 1)).to.eql( 'bytes' ); await PageObjects.lens.dragFieldToDimensionTrigger( '@message.raw', - 'lnsDatatable_column > lns-empty-dimension' + 'lnsDatatable_rows > lns-empty-dimension' ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows', 2)).to.eql( 'Top values of @message.raw' ); }); it('should reorder the elements for the table', async () => { - await PageObjects.lens.reorderDimensions('lnsDatatable_column', 3, 1); + await PageObjects.lens.reorderDimensions('lnsDatatable_rows', 3, 1); await PageObjects.lens.waitForVisualization(); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_rows')).to.eql([ 'Top values of @message.raw', 'Top values of clientip', 'bytes', diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 946f3a1dcba95..62e9b39d208b5 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.switchToVisualization('lnsDatatable'); - await PageObjects.lens.removeDimension('lnsDatatable_column'); + await PageObjects.lens.removeDimension('lnsDatatable_rows'); await PageObjects.lens.switchToVisualization('bar_stacked'); await PageObjects.lens.configureDimension({ @@ -446,7 +446,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsDatatable'); await PageObjects.lens.configureDimension({ - dimension: 'lnsDatatable_column > lns-empty-dimension', + dimension: 'lnsDatatable_rows > lns-empty-dimension', operation: 'date_histogram', field: '@timestamp', }); diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index 2d96458523cb6..f0f3ce27f4c31 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -59,16 +59,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('Average of bytes'); - await PageObjects.lens.toggleColumnVisibility('lnsDatatable_column > lns-dimensionTrigger'); + await PageObjects.lens.toggleColumnVisibility('lnsDatatable_rows > lns-dimensionTrigger'); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('Average of bytes'); - await PageObjects.lens.toggleColumnVisibility('lnsDatatable_column > lns-dimensionTrigger'); + await PageObjects.lens.toggleColumnVisibility('lnsDatatable_rows > lns-dimensionTrigger'); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('Top values of ip'); expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('Average of bytes'); }); + + it('should allow to transpose columns', async () => { + await PageObjects.lens.dragDimensionToDimension( + 'lnsDatatable_rows > lns-dimensionTrigger', + 'lnsDatatable_columns > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours'); + expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal( + '169.228.188.120 › Average of bytes' + ); + expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal( + '78.83.247.30 › Average of bytes' + ); + expect(await PageObjects.lens.getDatatableHeaderText(3)).to.equal( + '226.82.228.233 › Average of bytes' + ); + }); + + it('should allow to sort by transposed columns', async () => { + await PageObjects.lens.changeTableSortingBy(2, 'ascending'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDatatableCellText(0, 2)).to.eql('17,246'); + }); }); } diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 9847923c1bf5b..8af2e45b59838 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -11,8 +11,7 @@ export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const security = getService('security'); - // FLAKY: https://github.com/elastic/kibana/issues/93737 - describe.skip('auto fit map to bounds', () => { + describe('auto fit map to bounds', () => { describe('initial location', () => { before(async () => { await security.testUser.setRoles(['global_maps_all', 'test_logstash_reader']); diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 2663242406a75..4da36a44cff08 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -84,8 +84,7 @@ export default function ({ getPageObjects, getService }) { expect(beforeQueryRefreshTimestamp).not.to.equal(afterQueryRefreshTimestamp); }); - // https://github.com/elastic/kibana/issues/93718 - it.skip('should apply query to fit to bounds', async () => { + it('should apply query to fit to bounds', async () => { // Set view to other side of world so no matching results await PageObjects.maps.setView(-15, -100, 6); await PageObjects.maps.clickFitToBounds('logstash'); diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/visualize_create_menu.js index bac879dd9c81d..c9044353fbde8 100644 --- a/x-pack/test/functional/apps/maps/visualize_create_menu.js +++ b/x-pack/test/functional/apps/maps/visualize_create_menu.js @@ -29,9 +29,8 @@ export default function ({ getService, getPageObjects }) { it('should take users to Maps application when Maps is clicked', async () => { await PageObjects.visualize.clickMapsApp(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.maps.waitForLayersToLoad(); - const doesLayerExist = await PageObjects.maps.doesLayerExist('Road map'); - expect(doesLayerExist).to.equal(true); + const onMapPage = await PageObjects.maps.onMapPage(); + expect(onMapPage).to.equal(true); }); }); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 5d6a355939d30..79f869040f74a 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -150,7 +150,7 @@ "type": "envelope" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", "title": "document example", "uiStateJSON": "{\"isDarkMode\":false}" @@ -181,7 +181,7 @@ "type": "envelope" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"machine.os.raw : \\\"ios\\\"\",\"language\":\"kuery\"}}", "title": "document example with query", "uiStateJSON": "{\"isDarkMode\":false}" @@ -212,7 +212,7 @@ "type": "envelope" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "description": "", "mapStateJSON": "{\"zoom\":4.09,\"center\":{\"lon\":-100.58836,\"lat\":33.21778},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"index\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"machine.os.raw\",\"value\":\"ios\",\"params\":{\"query\":\"ios\"}},\"query\":{\"match\":{\"machine.os.raw\":{\"query\":\"ios\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}]}", "title": "document example with filter", @@ -288,7 +288,7 @@ "title" : "document example top hits split with scripted field", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-24T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"scalingType\":\"TOP_HITS\",\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"scalingType\":\"TOP_HITS\",\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -344,7 +344,7 @@ "title" : "document example with data driven styles", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"hour_of_day\",\"name\":\"hour_of_day\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"hour_of_day\",\"name\":\"hour_of_day\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -400,7 +400,7 @@ "title" : "document example with data driven styles on date field", "description" : "", "mapStateJSON": "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"@timestamp\",\"name\":\"@timestamp\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"@timestamp\",\"name\":\"@timestamp\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -456,7 +456,7 @@ "title" : "document example hidden", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":false,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":false,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" }, "type" : "map", @@ -681,7 +681,7 @@ "type": "polygon" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"frk92\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"7d807c75-088a-44b7-920a-e7e47f4fc038\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"1035e930-1811-11e9-b78a-23d706cd2507\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"frk92\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"7d807c75-088a-44b7-920a-e7e47f4fc038\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"1035e930-1811-11e9-b78a-23d706cd2507\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":8.8,\"center\":{\"lon\":-179.98743,\"lat\":-0.09561},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}", "title": "antimeridian points example", "uiStateJSON": "{\"isDarkMode\":false}" @@ -726,7 +726,7 @@ "type": "polygon" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"ad9fj\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"4e4b5628-dbdc-40bb-93f0-8a7a48be1141\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"502886a0-18f8-11e9-97c8-5da5e037299c\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"ad9fj\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"4e4b5628-dbdc-40bb-93f0-8a7a48be1141\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"502886a0-18f8-11e9-97c8-5da5e037299c\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":5.65,\"center\":{\"lon\":179.03193,\"lat\":0.09593},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"}}", "title": "antimeridian shapes example", "uiStateJSON": "{\"isDarkMode\":false}" @@ -964,7 +964,7 @@ "title" : "blended document example", "description" : "", "mapStateJSON" : "{\"zoom\":10.27,\"center\":{\"lon\":-83.70716,\"lat\":32.73679},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-23T00:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", - "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"43a70a86-00fd-43af-9e84-4d9fe2d7513d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{},\"type\":\"VECTOR_TILE\"},{\"id\":\"307c8495-89f7-431b-83d8-78724d9a8f72\",\"label\":\"logstash-*\",\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"id\":\"20fc58c3-3c0a-4c7b-9cdc-37552cafdc21\",\"tooltipProperties\":[],\"type\":\"ES_SEARCH\",\"scalingType\":\"CLUSTERS\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"type\":\"BLENDED_VECTOR\",\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true}}]", + "layerListJSON" : "[{\"id\":\"307c8495-89f7-431b-83d8-78724d9a8f72\",\"label\":\"logstash-*\",\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"id\":\"20fc58c3-3c0a-4c7b-9cdc-37552cafdc21\",\"tooltipProperties\":[],\"type\":\"ES_SEARCH\",\"scalingType\":\"CLUSTERS\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"type\":\"BLENDED_VECTOR\",\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true}}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -1020,7 +1020,7 @@ "title" : "document example - auto fit to bounds for initial location", "description" : "", "mapStateJSON" : "{\"zoom\":5.2,\"center\":{\"lon\":-67.80052,\"lat\":-55.25331},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"AUTO_FIT_TO_BOUNDS\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" }, "type" : "map", @@ -1047,7 +1047,7 @@ "source": { "map" : { "description":"shapes with mvt scaling", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"76b9fc1d-1e8a-4d2f-9f9e-6ba2b19f24bb\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]", + "layerListJSON":"[{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]", "mapStateJSON":"{\"zoom\":3.75,\"center\":{\"lon\":80.01106,\"lat\":3.65009},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", "title":"geo_shape_mvt", "uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 6694a494cf853..3f6b5691314bb 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -196,6 +196,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte return await listingTable.onListingPage('map'); } + async onMapPage() { + log.debug(`onMapPage`); + return await testSubjects.exists('mapLayerTOC', { + timeout: 5000, + }); + } + async searchForMapWithName(name: string) { log.debug(`searchForMapWithName: ${name}`); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 727f6493910ff..70e3d7c1b9b15 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -167,10 +167,10 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte async setSliderValue(testDataSubj: string, value: number) { const slider = await testSubjects.find(testDataSubj); - let currentValue = await slider.getAttribute('value'); - let currentDiff = +currentValue - +value; - await retry.tryForTime(60 * 1000, async () => { + const currentValue = await slider.getAttribute('value'); + const currentDiff = +currentValue - +value; + if (currentDiff === 0) { return true; } else { @@ -189,20 +189,13 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte } await retry.tryForTime(1000, async () => { const newValue = await slider.getAttribute('value'); - if (newValue !== currentValue) { - currentValue = newValue; - currentDiff = +currentValue - +value; - return true; - } else { + if (newValue === currentValue) { throw new Error(`slider value should have changed, but is still ${currentValue}`); } }); - - throw new Error(`slider value should be '${value}' (got '${currentValue}')`); + await this.assertSliderValue(testDataSubj, value); } }); - - await this.assertSliderValue(testDataSubj, value); }, async assertSliderValue(testDataSubj: string, expectedValue: number) { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts index 6d0a3255685f2..57f03a197b389 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts @@ -12,7 +12,9 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const telemetryTestResources = getService('telemetryTestResources'); - describe('security solution endpoint telemetry', () => { + // The source of the data for these tests have changed and need to be updated + // There are currently tests in the security_solution application being maintained + describe.skip('security solution endpoint telemetry', () => { after(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 4cf931a042221..f28545f83a890 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -32,7 +32,6 @@ export default function (providerContext: FtrProviderContext) { }); loadTestFile(require.resolve('./endpoint_list')); loadTestFile(require.resolve('./policy_details')); - loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./endpoint_telemetry')); loadTestFile(require.resolve('./trusted_apps_list')); loadTestFile(require.resolve('./fleet_integrations')); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts deleted file mode 100644 index 6ab2a3e584eb8..0000000000000 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ /dev/null @@ -1,280 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'timePicker', 'hosts', 'settings']); - const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - - /** - * Navigating to the hosts page must be done after data is loaded into ES otherwise - * the hosts page will display the empty default page and if we load data after that - * we'd have to set the source filter on the page. - */ - const navigateToHostsAndSetDate = async () => { - await pageObjects.hosts.navigateToSecurityHostsPage(); - await pageObjects.common.dismissBanner(); - const fromTime = 'Jan 1, 2018 @ 00:00:00.000'; - const toTime = 'now'; - await pageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - }; - - describe.skip('Endpoint Event Resolver', function () { - before(async () => { - await browser.setWindowSize(1800, 1200); - }); - after(async () => { - await pageObjects.hosts.deleteDataStreams(); - }); - - describe('Endpoint Resolver Tree', function () { - before(async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/functions', { useCreate: true }); - await navigateToHostsAndSetDate(); - await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.events.file'); - }); - after(async () => { - await pageObjects.hosts.deleteDataStreams(); - }); - - it('check that Resolver and Data table is loaded', async () => { - await testSubjects.existOrFail('resolver:graph'); - await testSubjects.existOrFail('tableHeaderCell_name_0'); - await testSubjects.existOrFail('tableHeaderCell_timestamp_1'); - }); - - it('compare resolver Nodes Table data and Data length', async () => { - const nodeData: string[] = []; - const TableData: string[] = []; - - const Table = await testSubjects.findAll('resolver:node-list:node-link:title'); - for (const value of Table) { - const text = await value._webElement.getText(); - TableData.push(text.split('\n')[0]); - } - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - const Nodes = await testSubjects.findAll('resolver:node:primary-button'); - for (const value of Nodes) { - nodeData.push(await value._webElement.getText()); - } - for (let i = 0; i < nodeData.length; i++) { - expect(TableData[i]).to.eql(nodeData[i]); - } - expect(nodeData.length).to.eql(TableData.length); - await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); - }); - - it('resolver Nodes navigation Up', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:north-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < OriginalNodeDataStyle.length; i++) { - expect(parseFloat(OriginalNodeDataStyle[i].top)).to.lessThan( - parseFloat(NewNodeDataStyle[i].top) - ); - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.equal( - parseFloat(NewNodeDataStyle[i].left) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Down', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:south-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].top)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].top) - ); - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.equal( - parseFloat(NewNodeDataStyle[i].left) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Left', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:east-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < OriginalNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].left)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.equal( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Right', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await testSubjects.click('resolver:graph-controls:west-button'); - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.lessThan( - parseFloat(NewNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.equal( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Center', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:east-button')).click(); - await (await testSubjects.find('resolver:graph-controls:south-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].left)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - const CenterNodeDataStyle = await pageObjects.hosts.parseStyles(); - - for (let i = 0; i < CenterNodeDataStyle.length; i++) { - expect(parseFloat(CenterNodeDataStyle[i].left)).to.equal( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(CenterNodeDataStyle[i].top)).to.equal( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - }); - - it('resolver Nodes navigation zoom in', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 1; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].left)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].top) - ); - expect(parseFloat(OriginalNodeDataStyle[i].width)).to.lessThan( - parseFloat(NewNodeDataStyle[i].width) - ); - expect(parseFloat(OriginalNodeDataStyle[i].height)).to.lessThan( - parseFloat(NewNodeDataStyle[i].height) - ); - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - } - }); - - it('resolver Nodes navigation zoom out', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - const NewNodeDataStyle1 = await pageObjects.hosts.parseStyles(); - for (let i = 1; i < OriginalNodeDataStyle.length; i++) { - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.lessThan( - parseFloat(NewNodeDataStyle1[i].left) - ); - expect(parseFloat(OriginalNodeDataStyle[i].top)).to.lessThan( - parseFloat(NewNodeDataStyle1[i].top) - ); - expect(parseFloat(NewNodeDataStyle1[i].width)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].width) - ); - expect(parseFloat(NewNodeDataStyle1[i].height)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].height) - ); - } - await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); - }); - }); - - describe('node related event pills', function () { - /** - * Verifies that the pills of a node have the correct text. - * - * @param id the node ID to verify the pills for. - * @param expectedPills a map of expected pills for all nodes - */ - const verifyPills = async (id: string, expectedPills: Set) => { - const relatedEventPills = await pageObjects.hosts.findNodePills(id); - expect(relatedEventPills.length).to.equal(expectedPills.size); - for (const pill of relatedEventPills) { - const pillText = await pill._webElement.getText(); - // check that we have the pill text in our expected map - expect(expectedPills.has(pillText)).to.equal(true); - } - }; - - before(async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/alert_events', { useCreate: true }); - await navigateToHostsAndSetDate(); - }); - after(async () => { - await pageObjects.hosts.deleteDataStreams(); - }); - - describe('endpoint.alerts filter', () => { - before(async () => { - await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.alerts'); - await pageObjects.hosts.clickZoomOut(); - await browser.setWindowSize(2100, 1500); - }); - - it('has the correct pill text', async () => { - const expectedData: Map> = new Map([ - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTc2MzYtMTMyNDc2MTQ0NDIuOTU5MTE2NjAw', - new Set(['1 library']), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTMxMTYtMTMyNDcyNDk0MjQuOTg4ODI4NjAw', - new Set(['157 file', '520 registry']), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTUwODQtMTMyNDc2MTQ0NDIuOTcyODQ3MjAw', - new Set(), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTg2OTYtMTMyNDc2MTQ0MjEuNjc1MzY0OTAw', - new Set(['3 file']), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTcyNjAtMTMyNDc2MTQ0MjIuMjQwNDI2MTAw', - new Set(), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTczMDAtMTMyNDc2MTQ0MjEuNjg2NzI4NTAw', - new Set(), - ], - ]); - - for (const [id, expectedPills] of expectedData.entries()) { - // center the node in the view - await pageObjects.hosts.clickNodeLinkInPanel(id); - await verifyPills(id, expectedPills); - } - }); - }); - }); - }); -} diff --git a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts deleted file mode 100644 index e7553e68d670b..0000000000000 --- a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts +++ /dev/null @@ -1,243 +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 { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; -import { nudgeAnimationDuration } from '../../../plugins/security_solution/public/resolver/store/camera/scaling_constants'; -import { FtrProviderContext } from '../ftr_provider_context'; -import { - deleteEventsStream, - deleteAlertsStream, - deleteMetadataStream, - deletePolicyStream, - deleteTelemetryStream, -} from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; - -export interface DataStyle { - left: string; - top: string; - width: string; - height: string; -} - -export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'header']); - const testSubjects = getService('testSubjects'); - const queryBar = getService('queryBar'); - const find = getService('find'); - - /** - * Returns the node IDs for the visible nodes in the resolver graph. - */ - const findVisibleNodeIDs = async (): Promise => { - const visibleNodes = await testSubjects.findAll('resolver:node'); - return Promise.all( - visibleNodes.map(async (node: WebElementWrapper) => { - return node.getAttribute('data-test-resolver-node-id'); - }) - ); - }; - - /** - * This assumes you are on the process list in the panel and will find and click the node - * with the given ID to bring it into view in the graph. - * - * @param id the ID of the node to find and click. - */ - const clickNodeLinkInPanel = async (id: string): Promise => { - await navigateToProcessListInPanel(); - const panelNodeButton = await find.byCssSelector( - `[data-test-subj='resolver:node-list:node-link'][data-test-node-id='${id}']` - ); - - await panelNodeButton?.click(); - // ensure that we wait longer than the animation time - await pageObjects.common.sleep(nudgeAnimationDuration * 2); - }; - - /** - * Finds all the pills for a particular node. - * - * @param id the ID of the node - */ - const findNodePills = async (id: string): Promise => { - return testSubjects.findAllDescendant( - 'resolver:map:node-submenu-item', - await find.byCssSelector( - `[data-test-subj='resolver:node'][data-test-resolver-node-id='${id}']` - ) - ); - }; - - /** - * Navigate back to the process list view in the panel. - */ - const navigateToProcessListInPanel = async () => { - const [ - isOnNodeListPage, - isOnCategoryPage, - isOnNodeDetailsPage, - isOnRelatedEventDetailsPage, - ] = await Promise.all([ - testSubjects.exists('resolver:node-list', { timeout: 1 }), - testSubjects.exists('resolver:node-events-in-category:breadcrumbs:node-list-link', { - timeout: 1, - }), - testSubjects.exists('resolver:node-detail:breadcrumbs:node-list-link', { timeout: 1 }), - testSubjects.exists('resolver:event-detail:breadcrumbs:node-list-link', { timeout: 1 }), - ]); - - if (isOnNodeListPage) { - return; - } else if (isOnCategoryPage) { - await ( - await testSubjects.find('resolver:node-events-in-category:breadcrumbs:node-list-link') - ).click(); - } else if (isOnNodeDetailsPage) { - await (await testSubjects.find('resolver:node-detail:breadcrumbs:node-list-link')).click(); - } else if (isOnRelatedEventDetailsPage) { - await (await testSubjects.find('resolver:event-detail:breadcrumbs:node-list-link')).click(); - } else { - // unknown page - return; - } - - await pageObjects.common.sleep(100); - }; - - /** - * Click the zoom out control. - */ - const clickZoomOut = async () => { - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - }; - - /** - * Navigate to Events Panel - */ - const navigateToEventsPanel = async () => { - const isFullScreen = await testSubjects.exists('exit-full-screen', { timeout: 400 }); - if (isFullScreen) { - await (await testSubjects.find('exit-full-screen')).click(); - } - - if (!(await testSubjects.exists('investigate-in-resolver-button', { timeout: 400 }))) { - await (await testSubjects.find('navigation-hosts')).click(); - await testSubjects.click('navigation-events'); - await testSubjects.existOrFail('event'); - } - }; - - /** - * @function parseStyles - * Parses a string of inline styles into a typescript object with casing for react - * @param {string} styles - * @returns {Object} - */ - const parseStyle = ( - styles: string - ): { - left?: string; - top?: string; - width?: string; - height?: string; - } => - styles - .split(';') - .filter((style: string) => style.split(':')[0] && style.split(':')[1]) - .map((style: string) => [ - style - .split(':')[0] - .trim() - .replace(/-./g, (c: string) => c.substr(1).toUpperCase()), - style.split(':').slice(1).join(':').trim(), - ]) - .reduce( - (styleObj: {}, style: string[]) => ({ - ...styleObj, - [style[0]]: style[1], - }), - {} - ); - - /** - * Navigate to the Security Hosts page - */ - const navigateToSecurityHostsPage = async () => { - await pageObjects.common.navigateToUrlWithBrowserHistory('security', '/hosts/AllHosts'); - await pageObjects.header.waitUntilLoadingHasFinished(); - }; - - /** - * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. - * It uses euiTableCellContent to avoid polluting the array data with the euiTableRowCell__mobileHeader data. - * @param dataTestSubj - * @param element - * @returns Promise - */ - const getEndpointEventResolverNodeData = async (dataTestSubj: string, element: string) => { - await testSubjects.exists(dataTestSubj); - const Elements = await testSubjects.findAll(dataTestSubj); - const $ = []; - for (const value of Elements) { - $.push(await value.getAttribute(element)); - } - return $; - }; - - /** - * Gets a array of not parsed styles and returns the Array of parsed styles. - * @returns Promise - */ - const parseStyles = async () => { - const tableData = await getEndpointEventResolverNodeData('resolver:node', 'style'); - const styles: DataStyle[] = []; - for (let i = 1; i < tableData.length; i++) { - const eachStyle = parseStyle(tableData[i]); - styles.push({ - top: eachStyle.top ?? '', - height: eachStyle.height ?? '', - left: eachStyle.left ?? '', - width: eachStyle.width ?? '', - }); - } - return styles; - }; - /** - * Deletes DataStreams from Index Management. - */ - const deleteDataStreams = async () => { - await deleteEventsStream(getService); - await deleteAlertsStream(getService); - await deletePolicyStream(getService); - await deleteMetadataStream(getService); - await deleteTelemetryStream(getService); - }; - - /** - * execute Query And Open Resolver - */ - const executeQueryAndOpenResolver = async (query: string) => { - await navigateToEventsPanel(); - await queryBar.setQuery(query); - await queryBar.submitQuery(); - await testSubjects.click('full-screen'); - await testSubjects.click('investigate-in-resolver-button'); - }; - - return { - navigateToProcessListInPanel, - findNodePills, - clickNodeLinkInPanel, - findVisibleNodeIDs, - clickZoomOut, - navigateToEventsPanel, - navigateToSecurityHostsPage, - parseStyles, - deleteDataStreams, - executeQueryAndOpenResolver, - }; -} diff --git a/x-pack/test/security_solution_endpoint/page_objects/index.ts b/x-pack/test/security_solution_endpoint/page_objects/index.ts index 44c4bb21787a4..961e5ae44716d 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/index.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/index.ts @@ -11,7 +11,6 @@ import { EndpointPolicyPageProvider } from './policy_page'; import { TrustedAppsPageProvider } from './trusted_apps_page'; import { EndpointPageUtils } from './page_utils'; import { IngestManagerCreatePackagePolicy } from './ingest_manager_create_package_policy_page'; -import { SecurityHostsPageProvider } from './hosts_page'; import { FleetIntegrations } from './fleet_integrations_page'; export const pageObjects = { @@ -21,6 +20,5 @@ export const pageObjects = { trustedApps: TrustedAppsPageProvider, endpointPageUtils: EndpointPageUtils, ingestManagerCreatePackagePolicy: IngestManagerCreatePackagePolicy, - hosts: SecurityHostsPageProvider, fleetIntegrations: FleetIntegrations, };