diff --git a/.eslintrc.js b/.eslintrc.js index 211aed1da7279c..20875a2c2913df 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1256,6 +1256,22 @@ module.exports = { }, }, + /** + * Discover overrides + */ + { + files: ['src/plugins/discover/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-expect-error': false, + }, + ], + }, + }, + /** * Enterprise Search overrides * NOTE: We also have a single rule at the bottom of the file that diff --git a/api_docs/spaces.json b/api_docs/spaces.json index 299bba81678a03..d633b0e653e171 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -38,7 +38,9 @@ }, ">" ], - "description": [], + "description": [ + "the space." + ], "source": { "path": "x-pack/plugins/spaces/public/space_avatar/space_attributes.ts", "lineNumber": 24 @@ -88,7 +90,9 @@ }, ">" ], - "description": [], + "description": [ + "the space." + ], "source": { "path": "x-pack/plugins/spaces/public/space_avatar/space_attributes.ts", "lineNumber": 64 @@ -138,7 +142,9 @@ }, ">" ], - "description": [], + "description": [ + "the space." + ], "source": { "path": "x-pack/plugins/spaces/public/space_avatar/space_attributes.ts", "lineNumber": 43 @@ -176,7 +182,9 @@ "text": "Space" } ], - "description": [], + "description": [ + "\nResponse format when querying for spaces." + ], "tags": [], "children": [ { @@ -184,10 +192,12 @@ "id": "def-public.GetSpaceResult.authorizedPurposes", "type": "Object", "label": "authorizedPurposes", - "description": [], + "description": [ + "\nA set of flags indicating which purposes the user is authorized for." + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 22 + "lineNumber": 51 }, "signature": [ "Record<", @@ -204,7 +214,7 @@ ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 21 + "lineNumber": 47 }, "initialIsOpen": false }, @@ -212,7 +222,9 @@ "id": "def-public.Space", "type": "Interface", "label": "Space", - "description": [], + "description": [ + "\nA Kibana Space." + ], "tags": [], "children": [ { @@ -220,10 +232,12 @@ "id": "def-public.Space.id", "type": "string", "label": "id", - "description": [], + "description": [ + "\nThe unique identifier for this space.\nThe id becomes part of the \"URL Identifier\" of the space.\n\nExample: an id of `marketing` would result in the URL identifier of `/s/marketing`." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 10 + "lineNumber": 19 } }, { @@ -231,10 +245,12 @@ "id": "def-public.Space.name", "type": "string", "label": "name", - "description": [], + "description": [ + "\nDisplay name for this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 11 + "lineNumber": 24 } }, { @@ -242,10 +258,12 @@ "id": "def-public.Space.description", "type": "string", "label": "description", - "description": [], + "description": [ + "\nOptional description for this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 12 + "lineNumber": 29 }, "signature": [ "string | undefined" @@ -256,10 +274,12 @@ "id": "def-public.Space.color", "type": "string", "label": "color", - "description": [], + "description": [ + "\nOptional color (hex code) for this space.\nIf neither `color` nor `imageUrl` is specified, then a color will be automatically generated." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 13 + "lineNumber": 35 }, "signature": [ "string | undefined" @@ -270,10 +290,12 @@ "id": "def-public.Space.initials", "type": "string", "label": "initials", - "description": [], + "description": [ + "\nOptional display initials for this space's avatar. Supports a maximum of 2 characters.\nIf initials are not provided, then they will be derived from the space name automatically.\n\nInitials are not displayed if an `imageUrl` has been specified." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 14 + "lineNumber": 43 }, "signature": [ "string | undefined" @@ -281,50 +303,58 @@ }, { "tags": [], - "id": "def-public.Space.disabledFeatures", - "type": "Array", - "label": "disabledFeatures", - "description": [], + "id": "def-public.Space.imageUrl", + "type": "string", + "label": "imageUrl", + "description": [ + "\nOptional base-64 encoded data image url to show as this space's avatar.\nThis setting takes precedence over any configured `color` or `initials`." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 15 + "lineNumber": 49 }, "signature": [ - "string[]" + "string | undefined" ] }, { "tags": [], - "id": "def-public.Space._reserved", - "type": "CompoundType", - "label": "_reserved", - "description": [], + "id": "def-public.Space.disabledFeatures", + "type": "Array", + "label": "disabledFeatures", + "description": [ + "\nThe set of feature ids that should be hidden within this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 16 + "lineNumber": 54 }, "signature": [ - "boolean | undefined" + "string[]" ] }, { - "tags": [], - "id": "def-public.Space.imageUrl", - "type": "string", - "label": "imageUrl", - "description": [], + "tags": [ + "private" + ], + "id": "def-public.Space._reserved", + "type": "CompoundType", + "label": "_reserved", + "description": [ + "\nIndicates that this space is reserved (system controlled).\nReserved spaces cannot be created or deleted by end-users." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 17 + "lineNumber": 61 }, "signature": [ - "string | undefined" + "boolean | undefined" ] } ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 9 + "lineNumber": 12 }, "initialIsOpen": false } @@ -336,10 +366,12 @@ "type": "Type", "label": "GetAllSpacesPurpose", "tags": [], - "description": [], + "description": [ + "\nThe set of purposes to retrieve spaces:\n- `any`: retrieves all spaces the user is authorized to see.\n- `copySavedObjectsIntoSpace`: retrieves all spaces the user is authorized to copy saved objects into.\n- `findSavedObjects`: retrieves all spaces the user is authorized to search within.\n- `shareSavedObjectsIntoSpace`: retrieves all spaces the user is authorized to share saved objects into." + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 15 + "lineNumber": 38 }, "signature": [ "\"any\" | \"copySavedObjectsIntoSpace\" | \"findSavedObjects\" | \"shareSavedObjectsIntoSpace\"" @@ -353,10 +385,12 @@ "type": "Type", "label": "SpacesPluginSetup", "tags": [], - "description": [], + "description": [ + "\nSetup contract for the Spaces plugin." + ], "source": { "path": "x-pack/plugins/spaces/public/plugin.tsx", - "lineNumber": 39 + "lineNumber": 42 }, "signature": [ "{}" @@ -369,10 +403,12 @@ "type": "Type", "label": "SpacesPluginStart", "tags": [], - "description": [], + "description": [ + "\nStart contract for the Spaces plugin." + ], "source": { "path": "x-pack/plugins/spaces/public/plugin.tsx", - "lineNumber": 40 + "lineNumber": 47 }, "signature": [ "SpacesApi" @@ -391,7 +427,9 @@ "signature": [ "(basePath: string, spaceId: string, requestedPath: string) => string" ], - "description": [], + "description": [ + "\nGiven a server base path, space id, and requested resource, this will construct a space-aware path\nthat includes a URL identifier with the space id.\n" + ], "children": [ { "id": "def-server.addSpaceIdToPath.$1", @@ -401,10 +439,12 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the server's base path." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 44 + "lineNumber": 62 } }, { @@ -415,10 +455,12 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the space id." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 45 + "lineNumber": 63 } }, { @@ -429,18 +471,22 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the requested path (e.g. `/app/dashboard`)." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 46 + "lineNumber": 64 } } ], "tags": [], - "returnComment": [], + "returnComment": [ + "the space-aware version of the requested path, inclusive of the server's base path." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 43 + "lineNumber": 61 }, "initialIsOpen": false } @@ -450,32 +496,42 @@ "id": "def-server.GetAllSpacesOptions", "type": "Interface", "label": "GetAllSpacesOptions", - "description": [], + "description": [ + "\nControls how spaces are retrieved." + ], "tags": [], "children": [ { - "tags": [], + "tags": [ + "see" + ], "id": "def-server.GetAllSpacesOptions.purpose", "type": "CompoundType", "label": "purpose", - "description": [], + "description": [ + "\nAn optional purpose describing how the set of spaces will be used.\nThe default purpose (`any`) will retrieve all spaces the user is authorized to see,\nwhereas a more specific purpose will retrieve all spaces the user is authorized to perform a specific action within.\n" + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 11 + "lineNumber": 21 }, "signature": [ "\"any\" | \"copySavedObjectsIntoSpace\" | \"findSavedObjects\" | \"shareSavedObjectsIntoSpace\" | undefined" ] }, { - "tags": [], + "tags": [ + "see" + ], "id": "def-server.GetAllSpacesOptions.includeAuthorizedPurposes", "type": "CompoundType", "label": "includeAuthorizedPurposes", - "description": [], + "description": [ + "\nSet to true to return a set of flags indicating which purposes the user is authorized for.\n" + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 12 + "lineNumber": 28 }, "signature": [ "boolean | undefined" @@ -484,7 +540,7 @@ ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 10 + "lineNumber": 13 }, "initialIsOpen": false }, @@ -509,7 +565,9 @@ "text": "Space" } ], - "description": [], + "description": [ + "\nResponse format when querying for spaces." + ], "tags": [], "children": [ { @@ -517,10 +575,12 @@ "id": "def-server.GetSpaceResult.authorizedPurposes", "type": "Object", "label": "authorizedPurposes", - "description": [], + "description": [ + "\nA set of flags indicating which purposes the user is authorized for." + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 22 + "lineNumber": 51 }, "signature": [ "Record<", @@ -537,7 +597,289 @@ ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 21 + "lineNumber": 47 + }, + "initialIsOpen": false + }, + { + "id": "def-server.ISpacesClient", + "type": "Interface", + "label": "ISpacesClient", + "description": [ + "\nClient interface for interacting with spaces." + ], + "tags": [], + "children": [ + { + "id": "def-server.ISpacesClient.getAll", + "type": "Function", + "label": "getAll", + "signature": [ + "(options?: ", + { + "pluginId": "spaces", + "scope": "common", + "docId": "kibSpacesPluginApi", + "section": "def-common.GetAllSpacesOptions", + "text": "GetAllSpacesOptions" + }, + " | undefined) => Promise<", + { + "pluginId": "spaces", + "scope": "common", + "docId": "kibSpacesPluginApi", + "section": "def-common.GetSpaceResult", + "text": "GetSpaceResult" + }, + "[]>" + ], + "description": [ + "\nRetrieve all available spaces." + ], + "children": [ + { + "id": "def-server.ISpacesClient.getAll.$1", + "type": "Object", + "label": "options", + "isRequired": false, + "signature": [ + { + "pluginId": "spaces", + "scope": "common", + "docId": "kibSpacesPluginApi", + "section": "def-common.GetAllSpacesOptions", + "text": "GetAllSpacesOptions" + }, + " | undefined" + ], + "description": [ + "controls which spaces are retrieved." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 34 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 34 + } + }, + { + "id": "def-server.ISpacesClient.get", + "type": "Function", + "label": "get", + "signature": [ + "(id: string) => Promise<", + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + }, + ">" + ], + "description": [ + "\nRetrieve a space by its id." + ], + "children": [ + { + "id": "def-server.ISpacesClient.get.$1", + "type": "string", + "label": "id", + "isRequired": true, + "signature": [ + "string" + ], + "description": [ + "the space id." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 40 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 40 + } + }, + { + "id": "def-server.ISpacesClient.create", + "type": "Function", + "label": "create", + "signature": [ + "(space: ", + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + }, + ") => Promise<", + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + }, + ">" + ], + "description": [ + "\nCreates a space." + ], + "children": [ + { + "id": "def-server.ISpacesClient.create.$1", + "type": "Object", + "label": "space", + "isRequired": true, + "signature": [ + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + } + ], + "description": [ + "the space to create." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 46 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 46 + } + }, + { + "id": "def-server.ISpacesClient.update", + "type": "Function", + "label": "update", + "signature": [ + "(id: string, space: ", + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + }, + ") => Promise<", + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + }, + ">" + ], + "description": [ + "\nUpdates a space." + ], + "children": [ + { + "id": "def-server.ISpacesClient.update.$1", + "type": "string", + "label": "id", + "isRequired": true, + "signature": [ + "string" + ], + "description": [ + "the id of the space to update." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 53 + } + }, + { + "id": "def-server.ISpacesClient.update.$2", + "type": "Object", + "label": "space", + "isRequired": true, + "signature": [ + { + "pluginId": "spacesOss", + "scope": "common", + "docId": "kibSpacesOssPluginApi", + "section": "def-common.Space", + "text": "Space" + } + ], + "description": [ + "the updated space." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 53 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 53 + } + }, + { + "id": "def-server.ISpacesClient.delete", + "type": "Function", + "label": "delete", + "signature": [ + "(id: string) => Promise" + ], + "description": [ + "\nDeletes a space, and all saved objects belonging to that space." + ], + "children": [ + { + "id": "def-server.ISpacesClient.delete.$1", + "type": "string", + "label": "id", + "isRequired": true, + "signature": [ + "string" + ], + "description": [ + "the id of the space to delete." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 59 + } + } + ], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 59 + } + } + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", + "lineNumber": 29 }, "initialIsOpen": false }, @@ -545,7 +887,9 @@ "id": "def-server.Space", "type": "Interface", "label": "Space", - "description": [], + "description": [ + "\nA Kibana Space." + ], "tags": [], "children": [ { @@ -553,10 +897,12 @@ "id": "def-server.Space.id", "type": "string", "label": "id", - "description": [], + "description": [ + "\nThe unique identifier for this space.\nThe id becomes part of the \"URL Identifier\" of the space.\n\nExample: an id of `marketing` would result in the URL identifier of `/s/marketing`." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 10 + "lineNumber": 19 } }, { @@ -564,10 +910,12 @@ "id": "def-server.Space.name", "type": "string", "label": "name", - "description": [], + "description": [ + "\nDisplay name for this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 11 + "lineNumber": 24 } }, { @@ -575,10 +923,12 @@ "id": "def-server.Space.description", "type": "string", "label": "description", - "description": [], + "description": [ + "\nOptional description for this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 12 + "lineNumber": 29 }, "signature": [ "string | undefined" @@ -589,10 +939,12 @@ "id": "def-server.Space.color", "type": "string", "label": "color", - "description": [], + "description": [ + "\nOptional color (hex code) for this space.\nIf neither `color` nor `imageUrl` is specified, then a color will be automatically generated." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 13 + "lineNumber": 35 }, "signature": [ "string | undefined" @@ -603,10 +955,12 @@ "id": "def-server.Space.initials", "type": "string", "label": "initials", - "description": [], + "description": [ + "\nOptional display initials for this space's avatar. Supports a maximum of 2 characters.\nIf initials are not provided, then they will be derived from the space name automatically.\n\nInitials are not displayed if an `imageUrl` has been specified." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 14 + "lineNumber": 43 }, "signature": [ "string | undefined" @@ -614,50 +968,58 @@ }, { "tags": [], - "id": "def-server.Space.disabledFeatures", - "type": "Array", - "label": "disabledFeatures", - "description": [], + "id": "def-server.Space.imageUrl", + "type": "string", + "label": "imageUrl", + "description": [ + "\nOptional base-64 encoded data image url to show as this space's avatar.\nThis setting takes precedence over any configured `color` or `initials`." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 15 + "lineNumber": 49 }, "signature": [ - "string[]" + "string | undefined" ] }, { "tags": [], - "id": "def-server.Space._reserved", - "type": "CompoundType", - "label": "_reserved", - "description": [], + "id": "def-server.Space.disabledFeatures", + "type": "Array", + "label": "disabledFeatures", + "description": [ + "\nThe set of feature ids that should be hidden within this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 16 + "lineNumber": 54 }, "signature": [ - "boolean | undefined" + "string[]" ] }, { - "tags": [], - "id": "def-server.Space.imageUrl", - "type": "string", - "label": "imageUrl", - "description": [], + "tags": [ + "private" + ], + "id": "def-server.Space._reserved", + "type": "CompoundType", + "label": "_reserved", + "description": [ + "\nIndicates that this space is reserved (system controlled).\nReserved spaces cannot be created or deleted by end-users." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 17 + "lineNumber": 61 }, "signature": [ - "string | undefined" + "boolean | undefined" ] } ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 9 + "lineNumber": 12 }, "initialIsOpen": false }, @@ -665,7 +1027,9 @@ "id": "def-server.SpacesServiceSetup", "type": "Interface", "label": "SpacesServiceSetup", - "description": [], + "description": [ + "\nThe Spaces service setup contract." + ], "tags": [], "children": [ { @@ -702,20 +1066,23 @@ }, "" ], - "description": [], + "description": [ + "the request." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 23 + "lineNumber": 27 } } ], "tags": [ - "deprecated" + "deprecated", + "removeBy" ], "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 23 + "lineNumber": 27 } }, { @@ -737,20 +1104,23 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the space id to convert." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 31 + "lineNumber": 36 } } ], "tags": [ - "deprecated" + "deprecated", + "removeBy" ], "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 31 + "lineNumber": 36 } }, { @@ -772,26 +1142,29 @@ "signature": [ "string | undefined" ], - "description": [], + "description": [ + "the namespace to convert." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 39 + "lineNumber": 45 } } ], "tags": [ - "deprecated" + "deprecated", + "removeBy" ], "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 39 + "lineNumber": 45 } } ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 16 + "lineNumber": 19 }, "initialIsOpen": false }, @@ -799,7 +1172,9 @@ "id": "def-server.SpacesServiceStart", "type": "Interface", "label": "SpacesServiceStart", - "description": [], + "description": [ + "\nThe Spaces service start contract." + ], "tags": [], "children": [ { @@ -812,7 +1187,7 @@ ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 46 + "lineNumber": 56 }, "signature": [ "(request: ", @@ -823,9 +1198,14 @@ "section": "def-server.KibanaRequest", "text": "KibanaRequest" }, - ") => Pick<", - "SpacesClient", - ", \"get\" | \"delete\" | \"create\" | \"update\" | \"getAll\">" + ") => ", + { + "pluginId": "spaces", + "scope": "server", + "docId": "kibSpacesPluginApi", + "section": "def-server.ISpacesClient", + "text": "ISpacesClient" + } ] }, { @@ -862,10 +1242,12 @@ }, "" ], - "description": [], + "description": [ + "the request." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 52 + "lineNumber": 62 } } ], @@ -873,7 +1255,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 52 + "lineNumber": 62 } }, { @@ -910,10 +1292,12 @@ }, "" ], - "description": [], + "description": [ + "the request." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 58 + "lineNumber": 68 } } ], @@ -921,7 +1305,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 58 + "lineNumber": 68 } }, { @@ -966,10 +1350,12 @@ }, "" ], - "description": [], + "description": [ + "the request." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 64 + "lineNumber": 74 } } ], @@ -977,7 +1363,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 64 + "lineNumber": 74 } }, { @@ -999,10 +1385,12 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the space id to convert." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 70 + "lineNumber": 80 } } ], @@ -1010,7 +1398,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 70 + "lineNumber": 80 } }, { @@ -1032,10 +1420,12 @@ "signature": [ "string | undefined" ], - "description": [], + "description": [ + "the namespace to convert." + ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 76 + "lineNumber": 86 } } ], @@ -1043,13 +1433,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 76 + "lineNumber": 86 } } ], "source": { "path": "x-pack/plugins/spaces/server/spaces_service/spaces_service.ts", - "lineNumber": 42 + "lineNumber": 51 }, "initialIsOpen": false } @@ -1061,10 +1451,12 @@ "type": "Type", "label": "GetAllSpacesPurpose", "tags": [], - "description": [], + "description": [ + "\nThe set of purposes to retrieve spaces:\n- `any`: retrieves all spaces the user is authorized to see.\n- `copySavedObjectsIntoSpace`: retrieves all spaces the user is authorized to copy saved objects into.\n- `findSavedObjects`: retrieves all spaces the user is authorized to search within.\n- `shareSavedObjectsIntoSpace`: retrieves all spaces the user is authorized to share saved objects into." + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 15 + "lineNumber": 38 }, "signature": [ "\"any\" | \"copySavedObjectsIntoSpace\" | \"findSavedObjects\" | \"shareSavedObjectsIntoSpace\"" @@ -1072,17 +1464,87 @@ "initialIsOpen": false }, { - "id": "def-server.ISpacesClient", + "id": "def-server.SpacesClientRepositoryFactory", "type": "Type", - "label": "ISpacesClient", - "tags": [], - "description": [], + "label": "SpacesClientRepositoryFactory", + "tags": [ + "private" + ], + "description": [ + "\nFor consumption by the security plugin only." + ], "source": { - "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client.ts", - "lineNumber": 27 + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts", + "lineNumber": 34 }, "signature": [ - "{ get: (id: string) => Promise; delete: (id: string) => Promise; create: (space: Space) => Promise; update: (id: string, space: Space) => Promise; getAll: (options?: GetAllSpacesOptions) => Promise; }" + "(request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", savedObjectsStart: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsServiceStart", + "text": "SavedObjectsServiceStart" + }, + ") => Pick<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsRepository", + "text": "SavedObjectsRepository" + }, + ", \"get\" | \"delete\" | \"create\" | \"bulkCreate\" | \"checkConflicts\" | \"deleteByNamespace\" | \"find\" | \"bulkGet\" | \"resolve\" | \"update\" | \"addToNamespaces\" | \"deleteFromNamespaces\" | \"bulkUpdate\" | \"removeReferencesTo\" | \"incrementCounter\" | \"openPointInTimeForType\" | \"closePointInTime\" | \"createPointInTimeFinder\">" + ], + "initialIsOpen": false + }, + { + "id": "def-server.SpacesClientWrapper", + "type": "Type", + "label": "SpacesClientWrapper", + "tags": [ + "private" + ], + "description": [ + "\nFor consumption by the security plugin only." + ], + "source": { + "path": "x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts", + "lineNumber": 25 + }, + "signature": [ + "(request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", baseClient: ", + { + "pluginId": "spaces", + "scope": "server", + "docId": "kibSpacesPluginApi", + "section": "def-server.ISpacesClient", + "text": "ISpacesClient" + }, + ") => ", + { + "pluginId": "spaces", + "scope": "server", + "docId": "kibSpacesPluginApi", + "section": "def-server.ISpacesClient", + "text": "ISpacesClient" + } ], "initialIsOpen": false } @@ -1092,18 +1554,25 @@ "id": "def-server.SpacesPluginSetup", "type": "Interface", "label": "SpacesPluginSetup", - "description": [], + "description": [ + "\nSetup contract for the Spaces plugin." + ], "tags": [], "children": [ { - "tags": [], + "tags": [ + "deprecated", + "removeBy" + ], "id": "def-server.SpacesPluginSetup.spacesService", "type": "Object", "label": "spacesService", - "description": [], + "description": [ + "\nService for interacting with spaces.\n" + ], "source": { "path": "x-pack/plugins/spaces/server/plugin.ts", - "lineNumber": 55 + "lineNumber": 64 }, "signature": [ { @@ -1116,27 +1585,43 @@ ] }, { - "tags": [], + "tags": [ + "private" + ], "id": "def-server.SpacesPluginSetup.spacesClient", "type": "Object", "label": "spacesClient", - "description": [], + "description": [ + "\nRegistries exposed for the security plugin to transparently provide authorization and audit logging." + ], "source": { "path": "x-pack/plugins/spaces/server/plugin.ts", - "lineNumber": 56 + "lineNumber": 70 }, "signature": [ "{ setClientRepositoryFactory: (factory: ", - "SpacesClientRepositoryFactory", + { + "pluginId": "spaces", + "scope": "server", + "docId": "kibSpacesPluginApi", + "section": "def-server.SpacesClientRepositoryFactory", + "text": "SpacesClientRepositoryFactory" + }, ") => void; registerClientWrapper: (wrapper: ", - "SpacesClientWrapper", + { + "pluginId": "spaces", + "scope": "server", + "docId": "kibSpacesPluginApi", + "section": "def-server.SpacesClientWrapper", + "text": "SpacesClientWrapper" + }, ") => void; }" ] } ], "source": { "path": "x-pack/plugins/spaces/server/plugin.ts", - "lineNumber": 54 + "lineNumber": 57 }, "lifecycle": "setup", "initialIsOpen": true @@ -1145,7 +1630,9 @@ "id": "def-server.SpacesPluginStart", "type": "Interface", "label": "SpacesPluginStart", - "description": [], + "description": [ + "\nStart contract for the Spaces plugin." + ], "tags": [], "children": [ { @@ -1153,10 +1640,12 @@ "id": "def-server.SpacesPluginStart.spacesService", "type": "Object", "label": "spacesService", - "description": [], + "description": [ + "Service for interacting with spaces." + ], "source": { "path": "x-pack/plugins/spaces/server/plugin.ts", - "lineNumber": 63 + "lineNumber": 89 }, "signature": [ { @@ -1171,7 +1660,7 @@ ], "source": { "path": "x-pack/plugins/spaces/server/plugin.ts", - "lineNumber": 62 + "lineNumber": 87 }, "lifecycle": "start", "initialIsOpen": true @@ -1187,7 +1676,9 @@ "signature": [ "(basePath: string, spaceId: string, requestedPath: string) => string" ], - "description": [], + "description": [ + "\nGiven a server base path, space id, and requested resource, this will construct a space-aware path\nthat includes a URL identifier with the space id.\n" + ], "children": [ { "id": "def-common.addSpaceIdToPath.$1", @@ -1197,10 +1688,12 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the server's base path." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 44 + "lineNumber": 62 } }, { @@ -1211,10 +1704,12 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the space id." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 45 + "lineNumber": 63 } }, { @@ -1225,18 +1720,22 @@ "signature": [ "string" ], - "description": [], + "description": [ + "the requested path (e.g. `/app/dashboard`)." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 46 + "lineNumber": 64 } } ], "tags": [], - "returnComment": [], + "returnComment": [ + "the space-aware version of the requested path, inclusive of the server's base path." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 43 + "lineNumber": 61 }, "initialIsOpen": false }, @@ -1247,7 +1746,9 @@ "signature": [ "(requestBasePath: string | null | undefined, serverBasePath: string | null | undefined) => { spaceId: string; pathHasExplicitSpaceIdentifier: boolean; }" ], - "description": [], + "description": [ + "\nExtracts the space id from the given path.\n" + ], "children": [ { "id": "def-common.getSpaceIdFromPath.$1", @@ -1257,10 +1758,12 @@ "signature": [ "string | null | undefined" ], - "description": [], + "description": [ + "The base path of the current request." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 13 + "lineNumber": 22 } }, { @@ -1271,18 +1774,24 @@ "signature": [ "string | null | undefined" ], - "description": [], + "description": [ + "The server's base path." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 14 + "lineNumber": 23 } } ], - "tags": [], - "returnComment": [], + "tags": [ + "private" + ], + "returnComment": [ + "the space id." + ], "source": { "path": "x-pack/plugins/spaces/common/lib/spaces_url_parser.ts", - "lineNumber": 12 + "lineNumber": 21 }, "initialIsOpen": false }, @@ -1346,32 +1855,42 @@ "id": "def-common.GetAllSpacesOptions", "type": "Interface", "label": "GetAllSpacesOptions", - "description": [], + "description": [ + "\nControls how spaces are retrieved." + ], "tags": [], "children": [ { - "tags": [], + "tags": [ + "see" + ], "id": "def-common.GetAllSpacesOptions.purpose", "type": "CompoundType", "label": "purpose", - "description": [], + "description": [ + "\nAn optional purpose describing how the set of spaces will be used.\nThe default purpose (`any`) will retrieve all spaces the user is authorized to see,\nwhereas a more specific purpose will retrieve all spaces the user is authorized to perform a specific action within.\n" + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 11 + "lineNumber": 21 }, "signature": [ "\"any\" | \"copySavedObjectsIntoSpace\" | \"findSavedObjects\" | \"shareSavedObjectsIntoSpace\" | undefined" ] }, { - "tags": [], + "tags": [ + "see" + ], "id": "def-common.GetAllSpacesOptions.includeAuthorizedPurposes", "type": "CompoundType", "label": "includeAuthorizedPurposes", - "description": [], + "description": [ + "\nSet to true to return a set of flags indicating which purposes the user is authorized for.\n" + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 12 + "lineNumber": 28 }, "signature": [ "boolean | undefined" @@ -1380,7 +1899,7 @@ ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 10 + "lineNumber": 13 }, "initialIsOpen": false }, @@ -1405,7 +1924,9 @@ "text": "Space" } ], - "description": [], + "description": [ + "\nResponse format when querying for spaces." + ], "tags": [], "children": [ { @@ -1413,10 +1934,12 @@ "id": "def-common.GetSpaceResult.authorizedPurposes", "type": "Object", "label": "authorizedPurposes", - "description": [], + "description": [ + "\nA set of flags indicating which purposes the user is authorized for." + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 22 + "lineNumber": 51 }, "signature": [ "Record<", @@ -1433,7 +1956,7 @@ ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 21 + "lineNumber": 47 }, "initialIsOpen": false } @@ -1462,10 +1985,12 @@ "type": "Type", "label": "GetAllSpacesPurpose", "tags": [], - "description": [], + "description": [ + "\nThe set of purposes to retrieve spaces:\n- `any`: retrieves all spaces the user is authorized to see.\n- `copySavedObjectsIntoSpace`: retrieves all spaces the user is authorized to copy saved objects into.\n- `findSavedObjects`: retrieves all spaces the user is authorized to search within.\n- `shareSavedObjectsIntoSpace`: retrieves all spaces the user is authorized to share saved objects into." + ], "source": { "path": "x-pack/plugins/spaces/common/types.ts", - "lineNumber": 15 + "lineNumber": 38 }, "signature": [ "\"any\" | \"copySavedObjectsIntoSpace\" | \"findSavedObjects\" | \"shareSavedObjectsIntoSpace\"" diff --git a/api_docs/spaces_oss.json b/api_docs/spaces_oss.json index a93aeefb3db6fc..8442d6cb5d5be5 100644 --- a/api_docs/spaces_oss.json +++ b/api_docs/spaces_oss.json @@ -8,10 +8,10 @@ "id": "def-public.LegacyUrlConflictProps", "type": "Interface", "label": "LegacyUrlConflictProps", - "description": [], - "tags": [ - "public" + "description": [ + "\nProperties for the LegacyUrlConflict component." ], + "tags": [], "children": [ { "tags": [], @@ -23,7 +23,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 252 + "lineNumber": 257 }, "signature": [ "string | undefined" @@ -39,7 +39,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 256 + "lineNumber": 261 } }, { @@ -52,7 +52,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 260 + "lineNumber": 265 } }, { @@ -65,13 +65,13 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 264 + "lineNumber": 269 } } ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 245 + "lineNumber": 250 }, "initialIsOpen": false }, @@ -79,10 +79,10 @@ "id": "def-public.ShareToSpaceFlyoutProps", "type": "Interface", "label": "ShareToSpaceFlyoutProps", - "description": [], - "tags": [ - "public" + "description": [ + "\nProperties for the ShareToSpaceFlyout." ], + "tags": [], "children": [ { "tags": [], @@ -94,7 +94,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 130 + "lineNumber": 135 }, "signature": [ { @@ -116,7 +116,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 136 + "lineNumber": 141 }, "signature": [ "string | undefined" @@ -132,7 +132,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 142 + "lineNumber": 147 }, "signature": [ "string | undefined" @@ -148,7 +148,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 149 + "lineNumber": 154 }, "signature": [ "boolean | undefined" @@ -164,7 +164,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 156 + "lineNumber": 161 }, "signature": [ "boolean | undefined" @@ -180,7 +180,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 164 + "lineNumber": 169 }, "signature": [ "\"within-space\" | \"outside-space\" | undefined" @@ -196,7 +196,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 170 + "lineNumber": 175 }, "signature": [ "((spacesToAdd: string[], spacesToRemove: string[]) => Promise) | undefined" @@ -212,7 +212,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 174 + "lineNumber": 179 }, "signature": [ "(() => void) | undefined" @@ -228,7 +228,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 178 + "lineNumber": 183 }, "signature": [ "(() => void) | undefined" @@ -237,7 +237,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 126 + "lineNumber": 131 }, "initialIsOpen": false }, @@ -245,10 +245,10 @@ "id": "def-public.ShareToSpaceSavedObjectTarget", "type": "Interface", "label": "ShareToSpaceSavedObjectTarget", - "description": [], - "tags": [ - "public" + "description": [ + "\nDescribes the target saved object during a share operation." ], + "tags": [], "children": [ { "tags": [], @@ -260,7 +260,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 188 + "lineNumber": 193 } }, { @@ -273,7 +273,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 192 + "lineNumber": 197 } }, { @@ -286,7 +286,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 196 + "lineNumber": 201 }, "signature": [ "string[]" @@ -302,7 +302,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 202 + "lineNumber": 207 }, "signature": [ "string | undefined" @@ -318,7 +318,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 208 + "lineNumber": 213 }, "signature": [ "string | undefined" @@ -334,7 +334,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 214 + "lineNumber": 219 }, "signature": [ "string | undefined" @@ -343,7 +343,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 184 + "lineNumber": 189 }, "initialIsOpen": false }, @@ -351,20 +351,22 @@ "id": "def-public.SpaceAvatarProps", "type": "Interface", "label": "SpaceAvatarProps", - "description": [], - "tags": [ - "public" + "description": [ + "\nProperties for the SpaceAvatar component." ], + "tags": [], "children": [ { "tags": [], "id": "def-public.SpaceAvatarProps.space", "type": "Object", "label": "space", - "description": [], + "description": [ + "The space to represent with an avatar." + ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 271 + "lineNumber": 277 }, "signature": [ "Partial<", @@ -383,10 +385,12 @@ "id": "def-public.SpaceAvatarProps.size", "type": "CompoundType", "label": "size", - "description": [], + "description": [ + "The size of the avatar." + ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 272 + "lineNumber": 280 }, "signature": [ "\"m\" | \"s\" | \"l\" | \"xl\" | undefined" @@ -397,10 +401,12 @@ "id": "def-public.SpaceAvatarProps.className", "type": "string", "label": "className", - "description": [], + "description": [ + "Optional CSS class(es) to apply." + ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 273 + "lineNumber": 283 }, "signature": [ "string | undefined" @@ -416,7 +422,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 279 + "lineNumber": 290 }, "signature": [ "boolean | undefined" @@ -425,7 +431,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 270 + "lineNumber": 275 }, "initialIsOpen": false }, @@ -433,10 +439,10 @@ "id": "def-public.SpaceListProps", "type": "Interface", "label": "SpaceListProps", - "description": [], - "tags": [ - "public" + "description": [ + "\nProperties for the SpaceList component." ], + "tags": [], "children": [ { "tags": [], @@ -448,7 +454,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 224 + "lineNumber": 229 }, "signature": [ "string[]" @@ -464,7 +470,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 231 + "lineNumber": 236 }, "signature": [ "number | undefined" @@ -480,7 +486,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 239 + "lineNumber": 244 }, "signature": [ "\"within-space\" | \"outside-space\" | undefined" @@ -489,7 +495,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 220 + "lineNumber": 225 }, "initialIsOpen": false }, @@ -497,20 +503,22 @@ "id": "def-public.SpacesApi", "type": "Interface", "label": "SpacesApi", - "description": [], - "tags": [ - "public" + "description": [ + "\nClient-side Spaces API." ], + "tags": [], "children": [ { "tags": [], "id": "def-public.SpacesApi.activeSpace$", "type": "Object", "label": "activeSpace$", - "description": [], + "description": [ + "\nObservable representing the currently active space.\nThe details of the space can change without a full page reload (such as display name, color, etc.)" + ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 18 + "lineNumber": 22 }, "signature": [ "Observable", @@ -540,13 +548,15 @@ }, ">" ], - "description": [], + "description": [ + "\nRetrieve the currently active space." + ], "children": [], "tags": [], "returnComment": [], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 19 + "lineNumber": 27 } }, { @@ -555,11 +565,11 @@ "type": "Object", "label": "ui", "description": [ - "\nUI API to use to add spaces capabilities to an application" + "\nUI components and services to add spaces capabilities to an application." ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 23 + "lineNumber": 32 }, "signature": [ { @@ -582,10 +592,10 @@ "id": "def-public.SpacesApiUi", "type": "Interface", "label": "SpacesApiUi", - "description": [], - "tags": [ - "public" + "description": [ + "\nUI components and services to add spaces capabilities to an application." ], + "tags": [], "children": [ { "tags": [], @@ -593,11 +603,11 @@ "type": "Object", "label": "components", "description": [ - "\nLazy-loadable {@link SpacesApiUiComponent | React components} to support the spaces feature." + "\nLazy-loadable {@link SpacesApiUiComponent | React components} to support the Spaces feature." ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 40 + "lineNumber": 47 }, "signature": [ { @@ -619,7 +629,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 61 + "lineNumber": 68 }, "signature": [ "(path: string, objectNoun?: string | undefined) => Promise" @@ -628,7 +638,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 36 + "lineNumber": 43 }, "initialIsOpen": false }, @@ -637,11 +647,9 @@ "type": "Interface", "label": "SpacesApiUiComponent", "description": [ - "\nReact UI components to be used to display the spaces feature in any application.\n" - ], - "tags": [ - "public" + "\nReact UI components to be used to display the Spaces feature in any application." ], + "tags": [], "children": [ { "tags": [], @@ -653,7 +661,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 73 + "lineNumber": 78 }, "signature": [ { @@ -684,7 +692,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 79 + "lineNumber": 84 }, "signature": [ { @@ -715,7 +723,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 88 + "lineNumber": 93 }, "signature": [ { @@ -746,7 +754,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 106 + "lineNumber": 111 }, "signature": [ { @@ -777,7 +785,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 110 + "lineNumber": 115 }, "signature": [ { @@ -801,7 +809,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 69 + "lineNumber": 74 }, "initialIsOpen": false }, @@ -826,7 +834,9 @@ "text": "SpacesApi" } ], - "description": [], + "description": [ + "\nOSS Spaces plugin start contract when the Spaces feature is enabled." + ], "tags": [], "children": [ { @@ -834,10 +844,12 @@ "id": "def-public.SpacesAvailableStartContract.isSpacesAvailable", "type": "boolean", "label": "isSpacesAvailable", - "description": [], + "description": [ + "Indicates if the Spaces feature is enabled." + ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 12 + "lineNumber": 16 }, "signature": [ "true" @@ -846,7 +858,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 11 + "lineNumber": 14 }, "initialIsOpen": false }, @@ -854,10 +866,10 @@ "id": "def-public.SpacesContextProps", "type": "Interface", "label": "SpacesContextProps", - "description": [], - "tags": [ - "public" + "description": [ + "\nProperties for the SpacesContext." ], + "tags": [], "children": [ { "tags": [], @@ -869,7 +881,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 120 + "lineNumber": 125 }, "signature": [ "string | undefined" @@ -878,7 +890,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 116 + "lineNumber": 121 }, "initialIsOpen": false }, @@ -886,18 +898,25 @@ "id": "def-public.SpacesUnavailableStartContract", "type": "Interface", "label": "SpacesUnavailableStartContract", - "description": [], - "tags": [], + "description": [ + "\nOSS Spaces plugin start contract when the Spaces feature is disabled." + ], + "tags": [ + "deprecated", + "removeBy" + ], "children": [ { "tags": [], "id": "def-public.SpacesUnavailableStartContract.isSpacesAvailable", "type": "boolean", "label": "isSpacesAvailable", - "description": [], + "description": [ + "Indicates if the Spaces feature is enabled." + ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 16 + "lineNumber": 26 }, "signature": [ "false" @@ -906,7 +925,7 @@ ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 15 + "lineNumber": 24 }, "initialIsOpen": false } @@ -917,15 +936,13 @@ "id": "def-public.LazyComponentFn", "type": "Type", "label": "LazyComponentFn", - "tags": [ - "public" - ], + "tags": [], "description": [ - "\nFunction that returns a promise for a lazy-loadable component.\n" + "\nFunction that returns a promise for a lazy-loadable component." ], "source": { "path": "src/plugins/spaces_oss/public/api.ts", - "lineNumber": 31 + "lineNumber": 38 }, "signature": [ "(props: T) => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" @@ -938,7 +955,9 @@ "id": "def-public.SpacesOssPluginSetup", "type": "Interface", "label": "SpacesOssPluginSetup", - "description": [], + "description": [ + "\nOSS Spaces plugin setup contract." + ], "tags": [], "children": [ { @@ -957,7 +976,7 @@ ") => void" ], "description": [ - "\nRegister a provider for the Spaces API.\n\nOnly one provider can be registered, subsequent calls to this method will fail." + "\nRegister a provider for the Spaces API.\n\nOnly one provider can be registered, subsequent calls to this method will fail.\n" ], "children": [ { @@ -974,24 +993,28 @@ "text": "SpacesApi" } ], - "description": [], + "description": [ + "the API provider." + ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 25 + "lineNumber": 42 } } ], - "tags": [], + "tags": [ + "private" + ], "returnComment": [], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 25 + "lineNumber": 42 } } ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 19 + "lineNumber": 32 }, "lifecycle": "setup", "initialIsOpen": true @@ -1001,10 +1024,12 @@ "type": "Type", "label": "SpacesOssPluginStart", "tags": [], - "description": [], + "description": [ + "\nOSS Spaces plugin start contract." + ], "source": { "path": "src/plugins/spaces_oss/public/types.ts", - "lineNumber": 28 + "lineNumber": 48 }, "signature": [ { @@ -1043,7 +1068,9 @@ "id": "def-common.Space", "type": "Interface", "label": "Space", - "description": [], + "description": [ + "\nA Kibana Space." + ], "tags": [], "children": [ { @@ -1051,10 +1078,12 @@ "id": "def-common.Space.id", "type": "string", "label": "id", - "description": [], + "description": [ + "\nThe unique identifier for this space.\nThe id becomes part of the \"URL Identifier\" of the space.\n\nExample: an id of `marketing` would result in the URL identifier of `/s/marketing`." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 10 + "lineNumber": 19 } }, { @@ -1062,10 +1091,12 @@ "id": "def-common.Space.name", "type": "string", "label": "name", - "description": [], + "description": [ + "\nDisplay name for this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 11 + "lineNumber": 24 } }, { @@ -1073,10 +1104,12 @@ "id": "def-common.Space.description", "type": "string", "label": "description", - "description": [], + "description": [ + "\nOptional description for this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 12 + "lineNumber": 29 }, "signature": [ "string | undefined" @@ -1087,10 +1120,12 @@ "id": "def-common.Space.color", "type": "string", "label": "color", - "description": [], + "description": [ + "\nOptional color (hex code) for this space.\nIf neither `color` nor `imageUrl` is specified, then a color will be automatically generated." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 13 + "lineNumber": 35 }, "signature": [ "string | undefined" @@ -1101,10 +1136,12 @@ "id": "def-common.Space.initials", "type": "string", "label": "initials", - "description": [], + "description": [ + "\nOptional display initials for this space's avatar. Supports a maximum of 2 characters.\nIf initials are not provided, then they will be derived from the space name automatically.\n\nInitials are not displayed if an `imageUrl` has been specified." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 14 + "lineNumber": 43 }, "signature": [ "string | undefined" @@ -1112,50 +1149,58 @@ }, { "tags": [], - "id": "def-common.Space.disabledFeatures", - "type": "Array", - "label": "disabledFeatures", - "description": [], + "id": "def-common.Space.imageUrl", + "type": "string", + "label": "imageUrl", + "description": [ + "\nOptional base-64 encoded data image url to show as this space's avatar.\nThis setting takes precedence over any configured `color` or `initials`." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 15 + "lineNumber": 49 }, "signature": [ - "string[]" + "string | undefined" ] }, { "tags": [], - "id": "def-common.Space._reserved", - "type": "CompoundType", - "label": "_reserved", - "description": [], + "id": "def-common.Space.disabledFeatures", + "type": "Array", + "label": "disabledFeatures", + "description": [ + "\nThe set of feature ids that should be hidden within this space." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 16 + "lineNumber": 54 }, "signature": [ - "boolean | undefined" + "string[]" ] }, { - "tags": [], - "id": "def-common.Space.imageUrl", - "type": "string", - "label": "imageUrl", - "description": [], + "tags": [ + "private" + ], + "id": "def-common.Space._reserved", + "type": "CompoundType", + "label": "_reserved", + "description": [ + "\nIndicates that this space is reserved (system controlled).\nReserved spaces cannot be created or deleted by end-users." + ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 17 + "lineNumber": 61 }, "signature": [ - "string | undefined" + "boolean | undefined" ] } ], "source": { "path": "src/plugins/spaces_oss/common/types.ts", - "lineNumber": 9 + "lineNumber": 12 }, "initialIsOpen": false } diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index fc7101bfdd85b6..f4f226ec8728f0 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -71,6 +71,7 @@ yarn kbn watch-bazel - @kbn/babel-preset - @kbn/config - @kbn/config-schema +- @kbn/crypto - @kbn/dev-utils - @kbn/es - @kbn/eslint-import-resolver-kibana diff --git a/docs/index.asciidoc b/docs/index.asciidoc index eb6f794434f8a1..70e40576fdd71a 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -9,10 +9,12 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] -:docker-repo: docker.elastic.co/kibana/kibana -:docker-image: docker.elastic.co/kibana/kibana:{version} -:blob: {kib-repo}blob/{branch}/ -:security-ref: https://www.elastic.co/community/security/ +:docker-repo: docker.elastic.co/kibana/kibana +:docker-image: {docker-repo}:{version} +:es-docker-repo: docker.elastic.co/elasticsearch/elasticsearch +:es-docker-image: {es-docker-repo}:{version} +:blob: {kib-repo}blob/{branch}/ +:security-ref: https://www.elastic.co/community/security/ include::{docs-root}/shared/attributes.asciidoc[] diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 4d6dcb631792ea..6cdb1dbfa712e5 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -106,6 +106,12 @@ New connectors can be created by clicking the *Create connector* button, which w [role="screenshot"] image::images/connector-select-type.png[Connector select type] +[float] +[[importing-and-exporting-connectors]] +=== Importing and exporting connectors + +To import and export rules, use the <>. + [float] [[create-connectors]] === Preconfigured connectors diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 31e7b25eb66b18..9cc0d4cfe83f99 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -15,34 +15,55 @@ These images contain both free and subscription features. <> to try out all of the features. [float] -[[pull-image]] -=== Pull the image - -Obtaining Kibana for Docker is as simple as issuing a +docker pull+ command -against the Elastic Docker registry. +[[run-kibana-on-docker-for-dev]] +=== Run {kib} on Docker for development ifeval::["{release-state}"=="unreleased"] -However, version {version} of Kibana has not yet been released, so no Docker -image is currently available for this version. +NOTE: No Docker images are currently available for {kib} {version}. endif::[] ifeval::["{release-state}"!="unreleased"] -["source","txt",subs="attributes"] --------------------------------------------- -docker pull {docker-repo}:{version} --------------------------------------------- +To start an {es} container for development or testing, run: -[float] -=== Run Kibana on Docker for development -Kibana can be quickly started and connected to a local Elasticsearch container for development -or testing use with the following command: +[source,sh,subs="attributes"] +---- +docker network create elastic +docker pull {es-docker-image} +docker run --name es01-test --net elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" {es-docker-image} +---- + +To start {kib} and connect it to your {es} container, run the following commands +in a new terminal session: [source,sh,subs="attributes"] ---- -docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version} +docker pull {docker-image} +docker run --name kib01-test --net elastic -p 5601:5601 -e "ELASTICSEARCH_HOSTS=http://es01-test:9200" {docker-image} +---- + +To access {kib}, go to http://localhost:5601[http://localhost:5601]. + +[float] +=== Stop Docker containers + +To stop your containers, run: + +[source,sh] +---- +docker stop es01-test +docker stop kib01-test +---- + +To remove the containers and their network, run: + +[source,sh] +---- +docker network rm elastic +docker rm es01-test +docker rm kib01-test ---- endif::[] diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index b908bd03b09927..b15c46254b770e 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -57,6 +57,12 @@ These operations can also be performed in bulk by multi-selecting rules and clic [role="screenshot"] image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk] +[float] +[[importing-and-exporting-rules]] +=== Importing and exporting rules + +To import and export rules, use the <>. + [float] === Required permissions diff --git a/package.json b/package.json index 12ed4c46993436..dc0521e03deaa9 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module", "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", - "@kbn/crypto": "link:packages/kbn-crypto", + "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", "@kbn/i18n": "link:packages/kbn-i18n", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", @@ -398,6 +398,7 @@ "utility-types": "^3.10.0", "uuid": "3.3.2", "vega": "^5.19.1", + "vega-interpreter": "^1.0.4", "vega-lite": "^5.0.0", "vega-schema-url-parser": "^2.1.0", "vega-spec-injector": "^0.0.2", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index eb0d895133c8c8..10600e514cfb63 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -13,6 +13,7 @@ filegroup( "//packages/kbn-babel-preset:build", "//packages/kbn-config:build", "//packages/kbn-config-schema:build", + "//packages/kbn-crypto:build", "//packages/kbn-dev-utils:build", "//packages/kbn-es:build", "//packages/kbn-eslint-import-resolver-kibana:build", diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel new file mode 100644 index 00000000000000..3e23f17acaea0f --- /dev/null +++ b/packages/kbn-crypto/BUILD.bazel @@ -0,0 +1,87 @@ + +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-cypto" +PKG_REQUIRE_NAME = "@kbn/crypto" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "@npm//jest-styled-components", + "@npm//node-forge", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/node-forge", + "@npm//@types/testing-library__jest-dom", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/packages/kbn-crypto/package.json b/packages/kbn-crypto/package.json index 0787427c60b106..bbeb57e5b7cca6 100644 --- a/packages/kbn-crypto/package.json +++ b/packages/kbn-crypto/package.json @@ -4,10 +4,5 @@ "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", "main": "./target/index.js", - "types": "./target/index.d.ts", - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } -} \ No newline at end of file + "types": "./target/index.d.ts" +} diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index 5005152cac7546..a9f18fde1bba09 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -1,14 +1,13 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-crypto/src" }, - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/packages/kbn-server-http-tools/package.json b/packages/kbn-server-http-tools/package.json index 5a1bb0d5b536a0..c44bf17079aab5 100644 --- a/packages/kbn-server-http-tools/package.json +++ b/packages/kbn-server-http-tools/package.json @@ -10,10 +10,7 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, - "dependencies": { - "@kbn/crypto": "link:../kbn-crypto" - }, "devDependencies": { "@kbn/utility-types": "link:../kbn-utility-types" } -} \ No newline at end of file +} diff --git a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts b/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts index 71f1e0d9548695..3d9a51e3b1eba9 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/catch_retryable_es_client_errors.ts @@ -31,16 +31,16 @@ export const catchRetryableEsClientErrors = ( e instanceof EsErrors.ConnectionError || e instanceof EsErrors.TimeoutError || (e instanceof EsErrors.ResponseError && - (retryResponseStatuses.includes(e.statusCode) || + (retryResponseStatuses.includes(e?.statusCode) || // ES returns a 400 Bad Request when trying to close or delete an // index while snapshots are in progress. This should have been a 503 // so once https://github.com/elastic/elasticsearch/issues/65883 is // fixed we can remove this. - e.body?.error?.type === 'snapshot_in_progress_exception')) + e?.body?.error?.type === 'snapshot_in_progress_exception')) ) { return Either.left({ type: 'retryable_es_client_error' as const, - message: e.message, + message: e?.message, error: e, }); } else { diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index 84862839f7ed23..ba6aafbb2f651a 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -30,6 +30,11 @@ describe('actions', () => { elasticsearchClientMock.createErrorTransportRequestPromise(retryableError) ); + const nonRetryableError = new Error('crash'); + const clientWithNonRetryableError = elasticsearchClientMock.createInternalClient( + elasticsearchClientMock.createErrorTransportRequestPromise(nonRetryableError) + ); + describe('fetchIndices', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { const task = Actions.fetchIndices(client, ['my_index']); @@ -52,6 +57,11 @@ describe('actions', () => { } expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('re-throws non retry-able errors', async () => { + const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + await task(); + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); + }); }); describe('cloneIndex', () => { @@ -64,6 +74,11 @@ describe('actions', () => { } expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('re-throws non retry-able errors', async () => { + const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + await task(); + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); + }); }); describe('pickupUpdatedMappings', () => { @@ -156,6 +171,11 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('re-throws non retry-able errors', async () => { + const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + await task(); + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); + }); }); describe('waitForPickupUpdatedMappingsTask', () => { @@ -169,6 +189,11 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('re-throws non retry-able errors', async () => { + const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + await task(); + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); + }); }); describe('updateAliases', () => { @@ -182,6 +207,11 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('re-throws non retry-able errors', async () => { + const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + await task(); + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); + }); }); describe('createIndex', () => { @@ -195,6 +225,11 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('re-throws non retry-able errors', async () => { + const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + await task(); + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); + }); }); describe('updateAndPickupMappings', () => { diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 43afa746de140f..79261aecf675cf 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -107,34 +107,37 @@ export const setWriteBlock = ( IndexNotFound | RetryableEsClientError, 'set_write_block_succeeded' > => () => { - return client.indices - .addBlock<{ - acknowledged: boolean; - shards_acknowledged: boolean; - }>( - { - index, - block: 'write', - }, - { maxRetries: 0 /** handle retry ourselves for now */ } - ) - .then((res: any) => { - return res.body.acknowledged === true - ? Either.right('set_write_block_succeeded' as const) - : Either.left({ - type: 'retryable_es_client_error' as const, - message: 'set_write_block_failed', - }); - }) - .catch((e: ElasticsearchClientError) => { - if (e instanceof EsErrors.ResponseError) { - if (e.message === 'index_not_found_exception') { - return Either.left({ type: 'index_not_found_exception' as const, index }); + return ( + client.indices + .addBlock<{ + acknowledged: boolean; + shards_acknowledged: boolean; + }>( + { + index, + block: 'write', + }, + { maxRetries: 0 /** handle retry ourselves for now */ } + ) + // not typed yet + .then((res: any) => { + return res.body.acknowledged === true + ? Either.right('set_write_block_succeeded' as const) + : Either.left({ + type: 'retryable_es_client_error' as const, + message: 'set_write_block_failed', + }); + }) + .catch((e: ElasticsearchClientError) => { + if (e instanceof EsErrors.ResponseError) { + if (e.body?.error?.type === 'index_not_found_exception') { + return Either.left({ type: 'index_not_found_exception' as const, index }); + } } - } - throw e; - }) - .catch(catchRetryableEsClientErrors); + throw e; + }) + .catch(catchRetryableEsClientErrors) + ); }; /** @@ -263,12 +266,12 @@ export const cloneIndex = ( }); }) .catch((error: EsErrors.ResponseError) => { - if (error.body.error.type === 'index_not_found_exception') { + if (error?.body?.error?.type === 'index_not_found_exception') { return Either.left({ type: 'index_not_found_exception' as const, index: error.body.error.index, }); - } else if (error.body.error.type === 'resource_already_exists_exception') { + } else if (error?.body?.error?.type === 'resource_already_exists_exception') { /** * If the target index already exists it means a previous clone * operation had already been started. However, we can't be sure @@ -795,22 +798,22 @@ export const updateAliases = ( }) .catch((err: EsErrors.ElasticsearchClientError) => { if (err instanceof EsErrors.ResponseError) { - if (err.body.error.type === 'index_not_found_exception') { + if (err?.body?.error?.type === 'index_not_found_exception') { return Either.left({ type: 'index_not_found_exception' as const, index: err.body.error.index, }); } else if ( - err.body.error.type === 'illegal_argument_exception' && - err.body.error.reason.match( + err?.body?.error?.type === 'illegal_argument_exception' && + err?.body?.error?.reason?.match( /The provided expression \[.+\] matches an alias, specify the corresponding concrete indices instead./ ) ) { return Either.left({ type: 'remove_index_not_a_concrete_index' as const }); } else if ( - err.body.error.type === 'aliases_not_found_exception' || - (err.body.error.type === 'resource_not_found_exception' && - err.body.error.reason.match(/required alias \[.+\] does not exist/)) + err?.body?.error?.type === 'aliases_not_found_exception' || + (err?.body?.error?.type === 'resource_not_found_exception' && + err?.body?.error?.reason?.match(/required alias \[.+\] does not exist/)) ) { return Either.left({ type: 'alias_not_found_exception' as const, @@ -903,7 +906,7 @@ export const createIndex = ( }); }) .catch((error) => { - if (error.body.error.type === 'resource_already_exists_exception') { + if (error?.body?.error?.type === 'resource_already_exists_exception') { /** * If the target index already exists it means a previous create * operation had already been started. However, we can't be sure diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index 599c32137c553b..186962b568792f 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -14,6 +14,7 @@ import { schema as s, ObjectType } from '@kbn/config-schema'; * Currently supported: * - filter * - histogram + * - nested * - terms * * Not implemented: @@ -32,7 +33,6 @@ import { schema as s, ObjectType } from '@kbn/config-schema'; * - ip_range * - missing * - multi_terms - * - nested * - parent * - range * - rare_terms @@ -42,6 +42,7 @@ import { schema as s, ObjectType } from '@kbn/config-schema'; * - significant_text * - variable_width_histogram */ + export const bucketAggsSchemas: Record = { filter: s.object({ term: s.recordOf(s.string(), s.oneOf([s.string(), s.boolean(), s.number()])), @@ -71,6 +72,9 @@ export const bucketAggsSchemas: Record = { }) ), }), + nested: s.object({ + path: s.string(), + }), terms: s.object({ field: s.maybe(s.string()), collect_mode: s.maybe(s.string()), diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts b/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts index 8a7c1c3719eb0f..57421db76f5b6e 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts @@ -16,6 +16,12 @@ const mockMappings = { updated_at: { type: 'date', }, + references: { + type: 'nested', + properties: { + id: 'keyword', + }, + }, foo: { properties: { title: { @@ -182,6 +188,40 @@ describe('validateAndConvertAggregations', () => { }); }); + it('validates a nested root aggregations', () => { + expect( + validateAndConvertAggregations( + ['alert'], + { + aggName: { + nested: { + path: 'alert.references', + }, + aggregations: { + aggName2: { + terms: { field: 'alert.references.id' }, + }, + }, + }, + }, + mockMappings + ) + ).toEqual({ + aggName: { + nested: { + path: 'references', + }, + aggregations: { + aggName2: { + terms: { + field: 'references.id', + }, + }, + }, + }, + }); + }); + it('rewrites type attributes when valid', () => { const aggregations: AggsMap = { average: { @@ -428,4 +468,50 @@ describe('validateAndConvertAggregations', () => { `"[someAgg.aggs.nested.max.script]: definition for this key is missing"` ); }); + + it('throws an error when trying to access a property via {type}.{type}.attributes.{attr}', () => { + expect(() => { + validateAndConvertAggregations( + ['alert'], + { + aggName: { + cardinality: { + field: 'alert.alert.attributes.actions.group', + }, + aggs: { + aggName: { + max: { field: 'alert.alert.attributes.actions.group' }, + }, + }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + '"[aggName.cardinality.field] Invalid attribute path: alert.alert.attributes.actions.group"' + ); + }); + + it('throws an error when trying to access a property via {type}.{type}.{attr}', () => { + expect(() => { + validateAndConvertAggregations( + ['alert'], + { + aggName: { + cardinality: { + field: 'alert.alert.actions.group', + }, + aggs: { + aggName: { + max: { field: 'alert.alert.actions.group' }, + }, + }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + '"[aggName.cardinality.field] Invalid attribute path: alert.alert.actions.group"' + ); + }); }); diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation.ts b/src/core/server/saved_objects/service/lib/aggregations/validation.ts index a2fd392183132f..cd41a23f4a28b5 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation.ts @@ -56,10 +56,13 @@ const validateAggregations = ( aggregations: Record, context: ValidationContext ) => { - return Object.entries(aggregations).reduce((memo, [aggrName, aggrContainer]) => { - memo[aggrName] = validateAggregation(aggrContainer, childContext(context, aggrName)); - return memo; - }, {} as Record); + return Object.entries(aggregations).reduce>( + (memo, [aggrName, aggrContainer]) => { + memo[aggrName] = validateAggregation(aggrContainer, childContext(context, aggrName)); + return memo; + }, + {} + ); }; /** @@ -93,15 +96,18 @@ const validateAggregationContainer = ( container: estypes.AggregationContainer, context: ValidationContext ) => { - return Object.entries(container).reduce((memo, [aggName, aggregation]) => { - if (aggregationKeys.includes(aggName)) { - return memo; - } - return { - ...memo, - [aggName]: validateAggregationType(aggName, aggregation, childContext(context, aggName)), - }; - }, {} as estypes.AggregationContainer); + return Object.entries(container).reduce( + (memo, [aggName, aggregation]) => { + if (aggregationKeys.includes(aggName)) { + return memo; + } + return { + ...memo, + [aggName]: validateAggregationType(aggName, aggregation, childContext(context, aggName)), + }; + }, + {} + ); }; const validateAggregationType = ( @@ -143,7 +149,7 @@ const validateAggregationStructure = ( * }, * ``` */ -const attributeFields = ['field']; +const attributeFields = ['field', 'path']; /** * List of fields that have a Record as value * diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts b/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts index f817497e3759e9..0b2cc8e235c9cb 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts @@ -24,15 +24,17 @@ export const isRootLevelAttribute = ( allowedTypes: string[] ): boolean => { const splits = attributePath.split('.'); - if (splits.length !== 2) { + if (splits.length <= 1) { return false; } - const [type, fieldName] = splits; - if (allowedTypes.includes(fieldName)) { + const [type, firstPath, ...otherPaths] = splits; + if (allowedTypes.includes(firstPath)) { return false; } - return allowedTypes.includes(type) && fieldDefined(indexMapping, fieldName); + return ( + allowedTypes.includes(type) && fieldDefined(indexMapping, [firstPath, ...otherPaths].join('.')) + ); }; /** @@ -45,7 +47,8 @@ export const isRootLevelAttribute = ( * ``` */ export const rewriteRootLevelAttribute = (attributePath: string) => { - return attributePath.split('.')[1]; + const [, ...attributes] = attributePath.split('.'); + return attributes.join('.'); }; /** diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 4e12f116687344..479414c458466b 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -46,6 +46,8 @@ @include kbnThemeStyle('v8') { padding-bottom: $euiSizeS + 1px; + // Firefox adds margin to textarea + margin: 0; &.kbnQueryBar__textarea--hasPrepend { border-top-left-radius: 0; diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index e74046e9dc1ecc..4fbeac80b09723 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -83,7 +83,7 @@ const indexPattern = ({ indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; -indexPattern.formatField = (hit: Record, fieldName: string) => { +indexPattern.formatField = (hit: Record, fieldName: string) => { return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; }; diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 820e37d754ef2a..0316178862aa90 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -17,8 +17,10 @@ import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { + // eslint-disable-next-line @typescript-eslint/no-explicit-any fields: Record; sort: number[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any _source: Record; _id: string; } diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index 1f745ab1b728ef..fb0e58832a202e 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -28,11 +28,11 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - let afterTimeValue = afterTimeDoc.sort[0]; + let afterTimeValue: string | number = afterTimeDoc.sort[0]; if (nanoSeconds) { afterTimeValue = useNewFieldsApi - ? afterTimeDoc.fields[timeFieldName][0] - : afterTimeDoc._source[timeFieldName]; + ? (afterTimeDoc.fields[timeFieldName] as Array)[0] + : (afterTimeDoc._source[timeFieldName] as string | number); } return [afterTimeValue, afterTimeDoc.sort[1]]; } @@ -42,8 +42,8 @@ export function getEsQuerySearchAfter( searchAfter[0] = anchor.sort[0]; if (nanoSeconds) { searchAfter[0] = useNewFieldsApi - ? anchor.fields[timeFieldName][0] - : anchor._source[timeFieldName]; + ? (anchor.fields[timeFieldName] as Array)[0] + : (anchor._source[timeFieldName] as string | number); } searchAfter[1] = anchor.sort[1]; return searchAfter; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts b/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts index 0290675489e54b..c6f389ddca46a5 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts @@ -27,7 +27,6 @@ export function getFirstSortableField(indexPattern: IndexPattern, fieldNames: st const sortableFields = fieldNames.filter( (fieldName) => META_FIELD_NAMES.includes(fieldName) || - // @ts-ignore (indexPattern.fields.getByName(fieldName) || { sortable: false }).sortable ); return sortableFields[0]; diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts index ddcd88a1687883..dd8c874391fd4b 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts @@ -9,6 +9,7 @@ import { getAngularModule } from '../../../../../kibana_services'; import { ActionBar } from './action_bar'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any getAngularModule().directive('contextActionBar', function (reactDirective: any) { return reactDirective(ActionBar); }); diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts index 774a29721c426b..efadf2105074e5 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -// @ts-ignore +// @ts-expect-error import { getQueryParameterActions } from './actions'; import { FilterManager } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index b49a6546a993d3..ed4a74c70112bd 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -12,16 +12,17 @@ import { createBrowserHistory, History } from 'history'; import { FilterManager, Filter } from '../../../../data/public'; import { coreMock } from '../../../../../core/public/mocks'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; + const setupMock = coreMock.createSetup(); describe('Test Discover Context State', () => { let history: History; - let state: any; + let state: ReturnType; const getCurrentUrl = () => history.createHref(history.location); beforeEach(async () => { history = createBrowserHistory(); history.push('/'); - state = await getState({ + state = getState({ defaultStepSize: '4', timeFieldName: 'time', history, diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 0bae006ec1f6e4..8ac111a8fe087e 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -13,8 +13,8 @@ import { createStateContainer, createKbnUrlStateStorage, syncStates, - BaseStateContainer, withNotifyOnErrors, + ReduxLikeStateContainer, } from '../../../../kibana_utils/public'; import { esFilters, FilterManager, Filter, Query } from '../../../../data/public'; import { handleSourceColumnState } from './helpers'; @@ -85,11 +85,11 @@ interface GetStateReturn { /** * Global state, the _g part of the URL */ - globalState: BaseStateContainer; + globalState: ReduxLikeStateContainer; /** * App state, the _a part of the URL */ - appState: BaseStateContainer; + appState: ReduxLikeStateContainer; /** * Start sync between state and URL */ diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts index a41f6e62590418..0c033fd066eabe 100644 --- a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts +++ b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts @@ -12,7 +12,7 @@ import 'angular-mocks'; import 'angular-sanitize'; import 'angular-route'; -// @ts-ignore +// @ts-expect-error import { createDebounceProviderTimeout } from './debounce'; import { coreMock } from '../../../../../../../core/public/mocks'; import { initializeInnerAngularModule } from '../../../../get_inner_angular'; @@ -21,6 +21,7 @@ import { dataPluginMock } from '../../../../../../data/public/mocks'; import { initAngularBootstrap } from '../../../../../../kibana_legacy/public'; describe('debounce service', function () { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let debounce: (fn: () => void, timeout: number, options?: any) => any; let $timeout: ITimeoutService; let spy: SinonSpy; diff --git a/src/plugins/discover/public/application/angular/doc.ts b/src/plugins/discover/public/application/angular/doc.ts index 0745e213789564..27af3a96bbc84d 100644 --- a/src/plugins/discover/public/application/angular/doc.ts +++ b/src/plugins/discover/public/application/angular/doc.ts @@ -7,17 +7,17 @@ */ import { getAngularModule, getServices } from '../../kibana_services'; -// @ts-ignore import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; import html from './doc.html'; import { Doc } from '../components/doc/doc'; interface LazyScope extends ng.IScope { - [key: string]: any; + [key: string]: unknown; } const { timefilter } = getServices(); const app = getAngularModule(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any app.directive('discoverDoc', function (reactDirective: any) { return reactDirective( Doc, @@ -31,6 +31,7 @@ app.directive('discoverDoc', function (reactDirective: any) { ); }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any app.config(($routeProvider: any) => { $routeProvider .when('/doc/:indexPattern/:index/:type', { @@ -39,7 +40,7 @@ app.config(($routeProvider: any) => { // the new route, es 7 deprecated types, es 8 removed them .when('/doc/:indexPattern/:index', { // have to be written as function expression, because it's not compiled in dev mode - // eslint-disable-next-line object-shorthand + // eslint-disable-next-line @typescript-eslint/no-explicit-any, object-shorthand controller: function ($scope: LazyScope, $route: any) { timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); @@ -49,6 +50,7 @@ app.config(($routeProvider: any) => { $scope.indexPatternService = getServices().indexPatterns; }, template: html, + // eslint-disable-next-line @typescript-eslint/no-explicit-any k7Breadcrumbs: ($route: any) => [ ...getRootBreadcrumbs(), { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts index 8b1df567db6cb3..180da83beb2afd 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts @@ -9,10 +9,12 @@ import { ToolBarPagerText } from './tool_bar_pager_text'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createToolBarPagerTextDirective(reactDirective: any) { return reactDirective(ToolBarPagerText); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createToolBarPagerButtonsDirective(reactDirective: any) { return reactDirective(ToolBarPagerButtons); } diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts index dd6017674e5dcf..5e3a025d8c7baf 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts @@ -11,6 +11,7 @@ import { getServices } from '../../../../kibana_services'; import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; import { UI_SETTINGS } from '../../../../../../data/public'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx index 8519b1e81b0a47..57f7382bd98ca1 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { IndexPattern } from '../../../../../kibana_services'; -// @ts-ignore import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; import { getDefaultSort } from '../../lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index cf6b507edc070e..58ddf1eb7ba25b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -32,6 +32,7 @@ export function noWhiteSpace(html: string): string { const MIN_LINE_LENGTH = 20; interface LazyScope extends ng.IScope { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -94,6 +95,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { createSummaryRow($scope.row); }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any $scope.inlineFilter = function inlineFilter($event: any, type: string) { const column = $($event.currentTarget).data().column; const field = $scope.indexPattern.fields.getByName(column); @@ -119,6 +121,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { }; // create a tr element that lists the value for each *column* + // eslint-disable-next-line @typescript-eslint/no-explicit-any function createSummaryRow(row: any) { const indexPattern = $scope.indexPattern; $scope.flattenedRow = indexPattern.flattenHit(row); @@ -188,7 +191,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { const $cell = $cells.eq(i); if ($cell.data('discover:html') === html) return; - const reuse = find($cells.slice(i + 1), function (cell: any) { + const reuse = find($cells.slice(i + 1), (cell) => { return $.data(cell, 'discover:html') === html; }); @@ -222,6 +225,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { /** * Fill an element with the value of a field */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any function _displayField(row: any, fieldName: string, truncate = false) { const indexPattern = $scope.indexPattern; const text = indexPattern.formatField(row, fieldName); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index 0202f88e0e9029..b8e8bf1fe7d6ce 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -45,6 +45,7 @@ export type AngularScope = IScope & { renderProps?: DocTableLegacyProps }; export async function injectAngularElement( domNode: Element, template: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any renderProps: any, injector: auto.IInjectorService ) { @@ -64,6 +65,7 @@ export async function injectAngularElement( return newScope; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function getRenderFn(domNode: Element, props: any) { const directive = { template: ` { + $scope.$watch('hits', (hits: unknown[]) => { if (!hits) return; // Reset infinite scroll limit diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts index 82fa646513753c..f2377b61b51512 100644 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts @@ -9,6 +9,7 @@ import $ from 'jquery'; interface LazyScope extends ng.IScope { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -19,6 +20,7 @@ export function createInfiniteScrollDirective() { more: '=', }, link: ($scope: LazyScope, $element: JQuery) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let checkTimer: any; /** * depending on which version of Discover is displayed, different elements are scrolling diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts index c73656435fb588..f181d583f0211d 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts @@ -7,7 +7,7 @@ */ import { getDefaultSort } from './get_default_sort'; -// @ts-ignore +// @ts-expect-error import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; import { IndexPattern } from '../../../../kibana_services'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts index bd28987b4fdbd9..19d629e14da663 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts @@ -7,7 +7,7 @@ */ import { getSort, getSortArray } from './get_sort'; -// @ts-ignore +// @ts-expect-error import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; import { IndexPattern } from '../../../../kibana_services'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts index f0a13557af9fd4..dc7817d95dd38f 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts @@ -7,7 +7,7 @@ */ import { getSortForSearchSource } from './get_sort_for_search_source'; -// @ts-ignore +// @ts-expect-error import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; import { IndexPattern } from '../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts index 964ac585248b8f..7cd36d419969ec 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -// @ts-ignore +// @ts-expect-error import { Pager } from './pager'; export function createPagerFactory() { diff --git a/src/plugins/discover/public/application/angular/doc_viewer.tsx b/src/plugins/discover/public/application/angular/doc_viewer.tsx index 0b4f3c3cf036af..2b51b68b2fb340 100644 --- a/src/plugins/discover/public/application/angular/doc_viewer.tsx +++ b/src/plugins/discover/public/application/angular/doc_viewer.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { DocViewer } from '../components/doc_viewer/doc_viewer'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createDocViewerDirective(reactDirective: any) { return reactDirective( - (props: any) => { + (props: React.ComponentProps) => { return ; }, [ diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts index ca5cdbd8086061..80fb8a570f78b0 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -11,6 +11,7 @@ import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks'; import { setServices } from '../../../kibana_services'; +import { DiscoverServices } from '../../../build_services'; describe('Row formatter', () => { const hit = { @@ -59,11 +60,11 @@ describe('Row formatter', () => { beforeEach(() => { // @ts-expect-error indexPattern.formatHit = formatHitMock; - setServices({ + setServices(({ uiSettings: { get: () => 100, }, - }); + } as unknown) as DiscoverServices); }); it('formats document properly', () => { @@ -73,11 +74,11 @@ describe('Row formatter', () => { }); it('limits number of rendered items', () => { - setServices({ + setServices(({ uiSettings: { get: () => 1, }, - }); + } as unknown) as DiscoverServices); expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
"` ); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx b/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx index c5e64f38a31178..c410273cc75109 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx @@ -30,6 +30,7 @@ const TemplateComponent = ({ defPairs }: Props) => { ); }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const formatRow = (hit: Record, indexPattern: IndexPattern) => { const highlights = hit?.highlight ?? {}; // Keys are sorted in the hits object @@ -49,7 +50,9 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) }; export const formatTopLevelObject = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any row: Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any fields: Record, indexPattern: IndexPattern ) => { diff --git a/src/plugins/discover/public/application/angular/redirect.ts b/src/plugins/discover/public/application/angular/redirect.ts index a1717baf9226fa..5014376ff13afb 100644 --- a/src/plugins/discover/public/application/angular/redirect.ts +++ b/src/plugins/discover/public/application/angular/redirect.ts @@ -8,8 +8,10 @@ import { getAngularModule, getServices, getUrlTracker } from '../../kibana_services'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any getAngularModule().config(($routeProvider: any) => { $routeProvider.otherwise({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any resolveRedirectTo: ($rootScope: any) => { const path = window.location.hash.substr(1); getUrlTracker().restorePreviousUrl(); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index 5b0ef60b6079a0..fc64abfb510258 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -8,6 +8,7 @@ import { ContextAppLegacy } from './context_app_legacy'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createContextAppLegacy(reactDirective: any) { return reactDirective(ContextAppLegacy, [ ['filter', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx index 3edd8f2514e63d..cd3571f447cf53 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; -// @ts-ignore +// @ts-expect-error import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx index 83cb6981d761e7..37791e0350ef78 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -// @ts-ignore +// @ts-expect-error import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; export interface ContextErrorMessageProps { diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index cc88ef03c5d031..f8c74c07457aa1 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -7,6 +7,7 @@ */ import { Discover } from './discover'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createDiscoverDirective(reactDirective: any) { return reactDirective(Discover, [ ['fetch', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx index d55e46574e1a7a..39a9df89260238 100644 --- a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx @@ -33,6 +33,7 @@ export function DiscoverGridEmbeddable(props: DiscoverGridProps) { /** * this is just needed for the embeddable */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createDiscoverGridDirective(reactDirective: any) { return reactDirective(DiscoverGridEmbeddable, [ ['columns', { watchDepth: 'collection' }], diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx index 4684c9e49ab31e..965d3cb6a30c43 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -32,6 +32,7 @@ describe('Discover cell actions ', function () { const component = mountWithIntl( } rowIndex={1} columnId={'extension'} @@ -59,6 +60,7 @@ describe('Discover cell actions ', function () { const component = mountWithIntl( } rowIndex={1} columnId={'extension'} diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index d258c5d36ff75e..fc3dd499f92e08 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -115,7 +115,7 @@ export const getRenderCellValueFn = ( if (typeof rowFlattened[columnId] === 'object' && isDetails) { return ( } + json={rowFlattened[columnId] as Record} width={defaultMonacoEditorWidth} /> ); @@ -123,7 +123,8 @@ export const getRenderCellValueFn = ( if (field && field.type === '_source') { if (isDetails) { - return ; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ; } const formatted = indexPattern.formatHit(row); diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index deaaa1853ae9da..7367315eae7fbc 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -18,6 +18,7 @@ import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../. const mockSearchApi = jest.fn(); jest.mock('../../../kibana_services', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; return { @@ -46,6 +47,7 @@ jest.mock('../../../kibana_services', () => { }, }), getDocViewsRegistry: () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any addDocView(view: any) { registry.push(view); }, @@ -72,12 +74,14 @@ const waitForPromises = async () => * this works but logs ugly error messages until we're using React 16.9 * should be adapted when we upgrade */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any async function mountDoc(update = false, indexPatternGetter: any = null) { const indexPattern = { getComputedFields: () => [], }; const indexPatternService = { get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)), + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; const props = { diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index bbfca89e7ad4e8..f3a6b274649f5b 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -11,6 +11,7 @@ import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_d import { DocProps } from './doc'; import { Observable } from 'rxjs'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; +import { IndexPattern } from 'src/plugins/data/common'; const mockSearchResult = new Observable(); @@ -35,9 +36,9 @@ jest.mock('../../../kibana_services', () => ({ describe('Test of helper / hook', () => { test('buildSearchBody given useNewFieldsApi is false', () => { - const indexPattern = { + const indexPattern = ({ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), - } as any; + } as unknown) as IndexPattern; const actual = buildSearchBody('1', indexPattern, false); expect(actual).toMatchInlineSnapshot(` Object { @@ -59,9 +60,9 @@ describe('Test of helper / hook', () => { }); test('buildSearchBody useNewFieldsApi is true', () => { - const indexPattern = { + const indexPattern = ({ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), - } as any; + } as unknown) as IndexPattern; const actual = buildSearchBody('1', indexPattern, true); expect(actual).toMatchInlineSnapshot(` Object { @@ -88,7 +89,7 @@ describe('Test of helper / hook', () => { }); test('buildSearchBody with runtime fields', () => { - const indexPattern = { + const indexPattern = ({ getComputedFields: () => ({ storedFields: [], scriptFields: [], @@ -102,7 +103,7 @@ describe('Test of helper / hook', () => { }, }, }), - } as any; + } as unknown) as IndexPattern; const actual = buildSearchBody('1', indexPattern, true); expect(actual).toMatchInlineSnapshot(` Object { @@ -139,21 +140,22 @@ describe('Test of helper / hook', () => { const indexPattern = { getComputedFields: () => [], }; - const indexPatternService = { - get: jest.fn(() => Promise.resolve(indexPattern)), - } as any; - const props = { + const getMock = jest.fn(() => Promise.resolve(indexPattern)); + const indexPatternService = ({ + get: getMock, + } as unknown) as IndexPattern; + const props = ({ id: '1', index: 'index1', indexPatternId: 'xyz', indexPatternService, - } as DocProps; - let hook; + } as unknown) as DocProps; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let hook: any; await act(async () => { hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props }); }); - // @ts-ignore expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]); - expect(indexPatternService.get).toHaveBeenCalled(); + expect(getMock).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx index 6afa7f89371f98..de0353a020a677 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -14,6 +14,7 @@ import { getDocViewsRegistry } from '../../../kibana_services'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; jest.mock('../../../kibana_services', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; return { getServices: () => ({ @@ -22,6 +23,7 @@ jest.mock('../../../kibana_services', () => { }, }), getDocViewsRegistry: () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any addDocView(view: any) { registry.push(view); }, @@ -36,6 +38,7 @@ jest.mock('../../../kibana_services', () => { }); beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (getDocViewsRegistry() as any).resetRegistry(); jest.clearAllMocks(); }); @@ -44,6 +47,7 @@ test('Render with 3 different tabs', () => { const registry = getDocViewsRegistry(); registry.addDocView({ order: 10, title: 'Render function', render: jest.fn() }); registry.addDocView({ order: 20, title: 'React component', component: () =>
test
}); + // @ts-expect-error This should be invalid and will throw an error when rendering registry.addDocView({ order: 30, title: 'Invalid doc view' }); const renderProps = { hit: {} } as DocViewRenderProps; diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx index 1ad6500771d483..4ca53d929eeab1 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -16,11 +16,11 @@ import { getServices } from '../../../kibana_services'; import { KibanaContextProvider } from '../../../../../kibana_react/public'; interface Props { - component?: React.ComponentType; id: number; - render?: DocViewRenderFn; renderProps: DocViewRenderProps; title: string; + render?: DocViewRenderFn; + component?: React.ComponentType; } interface State { @@ -53,17 +53,11 @@ export class DocViewerTab extends React.Component { } render() { - const { component, render, renderProps, title } = this.props; + const { component: Component, render, renderProps, title } = this.props; const { hasError, error } = this.state; if (hasError && error) { return ; - } else if (!render && !component) { - return ( - - ); } if (render) { @@ -72,14 +66,20 @@ export class DocViewerTab extends React.Component { } // doc view is provided by a react component + if (Component) { + return ( + + + + + + ); + } - const Component = component as any; return ( - - - - - + ); } } diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx index 50a29dde858911..b8427bb6bbdd25 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx @@ -22,7 +22,7 @@ const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel' }); interface JsonCodeEditorProps { - json: Record; + json: Record; width?: string | number; hasLineNumbers?: boolean; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 54a2de14e2e26e..2041a8dfdc6379 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverField } from './discover_field'; @@ -49,7 +49,7 @@ function getComponent({ }) { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index 0113213f70c880..f82154af33d1ea 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverFieldDetails } from './discover_field_details'; @@ -18,7 +18,7 @@ import { getStubIndexPattern } from '../../../../../data/public/test_utils'; const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx index 07baeddf034eff..73e906cb62f5fc 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import { coreMock } from '../../../../../../core/public/mocks'; @@ -18,7 +18,7 @@ import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index ec680b062c9ba6..145053de1f21c0 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -47,7 +47,7 @@ describe('DiscoverFieldSearch', () => { const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { - // @ts-ignore + // @ts-expect-error (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); component.update(); @@ -66,7 +66,7 @@ describe('DiscoverFieldSearch', () => { // change value of aggregatable select const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { - // @ts-ignore + // @ts-expect-error (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); component.update(); @@ -74,14 +74,14 @@ describe('DiscoverFieldSearch', () => { // change value of searchable select const searchableButtonGroup = findButtonGroup(component, 'searchable'); act(() => { - // @ts-ignore + // @ts-expect-error (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-true', null); }); component.update(); expect(badge.text()).toEqual('2'); // change value of searchable select act(() => { - // @ts-ignore + // @ts-expect-error (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-any', null); }); component.update(); @@ -114,7 +114,7 @@ describe('DiscoverFieldSearch', () => { const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable'); const missingSwitch = findTestSubject(component, 'missingSwitch'); act(() => { - // @ts-ignore + // @ts-expect-error (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); missingSwitch.simulate('click'); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 73de3b14f88f6a..f6d577de564ade 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -14,7 +14,7 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; -import { IndexPattern } from 'src/plugins/data/public'; +import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; import { configMock } from '../../../__mocks__/config'; import { indexPatternsMock } from '../../../__mocks__/index_patterns'; @@ -28,14 +28,14 @@ const indexPattern1 = { attributes: { title: 'test1 title', }, -} as SavedObject; +} as SavedObject; const indexPattern2 = { id: 'the-index-pattern-id', attributes: { title: 'test2 title', }, -} as SavedObject; +} as SavedObject; const defaultProps = { config: configMock, @@ -58,6 +58,7 @@ function getIndexPatternPickerOptions(instance: ShallowWrapper) { function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any ).map((option: any) => option.label === selectedLabel ? { ...option, checked: 'on' } @@ -79,6 +80,7 @@ describe('DiscoverIndexPattern', () => { test('should list all index patterns', () => { const instance = shallow(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(getIndexPatternPickerOptions(instance)!.map((option: any) => option.label)).toEqual([ 'test1 title', 'test2 title', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx index 5a954270fdf587..6f9ff63d69782f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx @@ -9,7 +9,7 @@ import { getStubIndexPattern } from '../../../../../data/public/index_patterns/index_pattern.stub'; import { coreMock } from '../../../../../../core/public/mocks'; import { DiscoverServices } from '../../../build_services'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -56,7 +56,7 @@ const mockServices = ({ describe('Discover IndexPattern Management', () => { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 01541344be7e18..35d93095a1da31 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -9,9 +9,9 @@ import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import realHits from '../../../__fixtures__/real_hits.js'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -67,7 +67,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index caec61cc501b95..caa0e436f40910 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -9,9 +9,9 @@ import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import realHits from '../../../__fixtures__/real_hits.js'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -63,7 +63,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index faa31dde1bb80d..2cdd99774c2a8a 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -6,15 +6,17 @@ * Side Public License, v 1. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import _ from 'lodash'; -// @ts-ignore +// @ts-expect-error import realHits from '../../../../__fixtures__/real_hits.js'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../../__fixtures__/logstash_fields'; import { coreMock } from '../../../../../../../core/public/mocks'; import { IndexPattern } from '../../../../../../data/public'; import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; -// @ts-ignore +// @ts-expect-error import { fieldCalculator } from './field_calculator'; let indexPattern: IndexPattern; @@ -23,7 +25,7 @@ describe('fieldCalculator', function () { beforeEach(function () { indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index 2cea3270e5166a..1e35717d249f8b 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -// @ts-ignore +// @ts-expect-error import { fieldCalculator } from './field_calculator'; import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts index 68099fb0c8e2a0..e8eb07784cf9fc 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts @@ -167,18 +167,11 @@ describe('group_fields', function () { aggregatable: true, readFromDocValues: false, }; - const fieldsToGroup = [category, currency, currencyKeyword]; + const fieldsToGroup = [category, currency, currencyKeyword] as IndexPatternField[]; const fieldFilterState = getDefaultFieldFilter(); - const actual = groupFields( - fieldsToGroup as any, - ['currency'], - 5, - fieldCounts, - fieldFilterState, - true - ); + const actual = groupFields(fieldsToGroup, ['currency'], 5, fieldCounts, fieldFilterState, true); expect(actual.popular).toEqual([category]); expect(actual.selected).toEqual([currency]); diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 7539f29c1ec9db..e42288d52be8e7 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -11,6 +11,7 @@ import { mount } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; const indexPattern = ({ fields: { @@ -87,7 +88,7 @@ describe('DocViewTable at Discover', () => { scripted: 123, _underscore: 123, }, - } as any; + } as ElasticSearchHit; const props = { hit, @@ -185,7 +186,7 @@ describe('DocViewTable at Discover Context', () => { Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \ Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', }, - } as any; + } as ElasticSearchHit; const props = { hit, columns: ['extension'], @@ -312,7 +313,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { }, metaFields: ['_index', '_type', '_score', '_id'], flattenHit: jest.fn((hit) => { - const result = {} as Record; + const result = {} as Record; Object.keys(hit).forEach((key) => { if (key !== 'fields') { result[key] = hit[key]; @@ -325,7 +326,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { return result; }), formatHit: jest.fn((hit) => { - const result = {} as Record; + const result = {} as Record; Object.keys(hit).forEach((key) => { if (key !== 'fields') { result[key] = hit[key]; @@ -347,7 +348,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { _index: 'logstash-2014.09.09', _type: 'doc', _id: 'id123', - _score: null, + _score: 1.0, fields: { category: "Women's Clothing", 'category.keyword': "Women's Clothing", @@ -364,7 +365,6 @@ describe('DocViewTable at Discover Doc with Fields API', () => { onAddColumn: jest.fn(), onRemoveColumn: jest.fn(), }; - // @ts-ignore const component = mount(); it('renders multifield rows', () => { const categoryMultifieldRow = findTestSubject( diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index 74836711373b28..0da1fdd92d2cce 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -79,10 +79,10 @@ describe('timechart header', function () { component = mountWithIntl(); const dropdown = findTestSubject(component, 'discoverIntervalSelect'); expect(dropdown.length).toBe(1); - // @ts-ignore + // @ts-expect-error const values = dropdown.find('option').map((option) => option.prop('value')); expect(values).toEqual(['auto', 'ms', 's']); - // @ts-ignore + // @ts-expect-error const labels = dropdown.find('option').map((option) => option.text()); expect(labels).toEqual(['Auto', 'Millisecond', 'Second']); }); diff --git a/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx b/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx index 7acb56ea5535cf..a590a9c05eda4b 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx +++ b/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx @@ -32,6 +32,7 @@ export async function injectAngularElement( if (typeof Controller === 'function') { // when a controller is defined, expose the value it produces to the view as `$ctrl` // see: https://docs.angularjs.org/api/ng/provider/$compileProvider#component + // eslint-disable-next-line @typescript-eslint/no-explicit-any (newScope as any).$ctrl = $injector.instantiate(Controller, { $scope: newScope, }); diff --git a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts b/src/plugins/discover/public/application/doc_views/doc_views_registry.ts index da36be1b53a789..ad341d07aae5ab 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_registry.ts @@ -25,7 +25,8 @@ export class DocViewsRegistry { const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw; if (docView.directive) { // convert angular directive to render function for backwards compatibility - docView.render = convertDirectiveToRenderFn(docView.directive, () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (docView.render as any) = convertDirectiveToRenderFn(docView.directive as any, () => { if (!this.angularInjectorGetter) { throw new Error('Angular was not initialized'); } diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 02ac951f7f57ca..58399f31e032f1 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -12,7 +12,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { IndexPattern } from '../../../../data/public'; export interface AngularDirective { - controller: (...injectedServices: any[]) => void; + controller: (...injectedServices: unknown[]) => void; template: string; } @@ -49,17 +49,34 @@ export type DocViewRenderFn = ( renderProps: DocViewRenderProps ) => () => void; -export interface DocViewInput { - component?: DocViewerComponent; - directive?: AngularDirective; +export interface BaseDocViewInput { order: number; - render?: DocViewRenderFn; shouldShow?: (hit: ElasticSearchHit) => boolean; title: string; } -export interface DocView extends DocViewInput { - shouldShow: (hit: ElasticSearchHit) => boolean; +export interface RenderDocViewInput extends BaseDocViewInput { + render: DocViewRenderFn; + component?: undefined; + directive?: undefined; } +interface ComponentDocViewInput extends BaseDocViewInput { + component: DocViewerComponent; + render?: undefined; + directive?: undefined; +} + +interface DirectiveDocViewInput extends BaseDocViewInput { + component?: undefined; + render?: undefined; + directive: ng.IDirective; +} + +export type DocViewInput = ComponentDocViewInput | RenderDocViewInput | DirectiveDocViewInput; + +export type DocView = DocViewInput & { + shouldShow: NonNullable; +}; + export type DocViewInputFn = () => DocViewInput; diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/application/helpers/breadcrumbs.ts index 1a190b38822405..8a8d0e7027c653 100644 --- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts +++ b/src/plugins/discover/public/application/helpers/breadcrumbs.ts @@ -21,6 +21,7 @@ export function getRootBreadcrumbs() { ]; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function getSavedSearchBreadcrumbs($route: any) { return [ ...getRootBreadcrumbs(), diff --git a/src/plugins/discover/public/application/helpers/calc_field_counts.ts b/src/plugins/discover/public/application/helpers/calc_field_counts.ts index 1f00faaf56c391..edeaa0b9bce52f 100644 --- a/src/plugins/discover/public/application/helpers/calc_field_counts.ts +++ b/src/plugins/discover/public/application/helpers/calc_field_counts.ts @@ -14,7 +14,7 @@ import { IndexPattern } from '../../kibana_services'; */ export function calcFieldCounts( counts = {} as Record, - rows: Array>, + rows: Array>, indexPattern: IndexPattern ) { for (const hit of rows) { diff --git a/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts b/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts index fd92dc8259a1f4..f87d5328046d8c 100644 --- a/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts +++ b/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts @@ -16,7 +16,7 @@ import { Query } from 'src/plugins/data/public'; * @return Object */ -export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query { +export function migrateLegacyQuery(query: Query | { [key: string]: unknown } | string): Query { // Lucene was the only option before, so language-less queries are all lucene if (!has(query, 'language')) { return { query, language: 'lucene' }; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index cf95d5a85b9f2b..19ee06aa517985 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -8,6 +8,7 @@ import { History } from 'history'; +import type { auto } from 'angular'; import { Capabilities, ChromeStart, @@ -57,7 +58,7 @@ export interface DiscoverServices { toastNotifications: ToastsStart; getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; - getEmbeddableInjector: any; + getEmbeddableInjector: () => Promise; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; indexPatternFieldEditor: IndexPatternFieldEditorStart; @@ -67,7 +68,7 @@ export async function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, context: PluginInitializerContext, - getEmbeddableInjector: any + getEmbeddableInjector: () => Promise ): Promise { const services = { savedObjectsClient: core.savedObjects.client, diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index d78e53d99f6a2c..c36312a87ce9ad 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -150,7 +150,7 @@ function createLocalStorageModule() { } const createLocalStorageService = function (type: string) { - return function ($window: any) { + return function ($window: ng.IWindowService) { return new Storage($window[type]); }; }; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index e4b0035ed0e031..c2ab4ae34c958d 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -15,21 +15,24 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; -let angularModule: any = null; +let angularModule: ng.IModule | null = null; let services: DiscoverServices | null = null; let uiActions: UiActionsStart; /** * set bootstrapped inner angular module */ -export function setAngularModule(module: any) { +export function setAngularModule(module: ng.IModule) { angularModule = module; } /** * get boostrapped inner angular module */ -export function getAngularModule() { +export function getAngularModule(): ng.IModule { + if (!angularModule) { + throw new Error('Discover angular module not yet available'); + } return angularModule; } @@ -40,7 +43,7 @@ export function getServices(): DiscoverServices { return services; } -export function setServices(newServices: any) { +export function setServices(newServices: DiscoverServices) { services = newServices; } diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index bdad98a457a5c7..0f57c5c0fa1381 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -22,10 +22,10 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { - savedSearchLoader: {} as any, - urlGenerator: { + savedSearchLoader: {} as DiscoverStart['savedSearchLoader'], + urlGenerator: ({ createUrl: jest.fn(), - } as any, + } as unknown) as DiscoverStart['urlGenerator'], }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 692704c92356ee..3f55ab76372bc4 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -164,7 +164,10 @@ export class DiscoverPlugin public initializeInnerAngular?: () => void; public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; - setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { + setup( + core: CoreSetup, + plugins: DiscoverSetupPlugins + ): DiscoverSetup { const baseUrl = core.http.basePath.prepend('/app/discover'); if (plugins.share) { @@ -190,7 +193,8 @@ export class DiscoverPlugin defaultMessage: 'JSON', }), order: 20, - component: ({ hit }) => , + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: ({ hit }) => , }); const { diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index 2d29111fd37573..765e8b36cc1ea0 100644 --- a/src/plugins/discover/public/url_generator.test.ts +++ b/src/plugins/discover/public/url_generator.test.ts @@ -31,7 +31,7 @@ const setup = async ({ useHash = false }: SetupParams = {}) => { }; beforeEach(() => { - // @ts-ignore + // @ts-expect-error hashedItemStore.storage = mockStorage; }); diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index b66c06db3e1200..070f0253f17e08 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -47,5 +47,5 @@ export const searchSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: searchMigrations as any, + migrations: searchMigrations, }; diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index feaf91409797a2..0c45db5cd779e5 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +// TODO: This needs to be removed and properly typed +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { flow, get } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; diff --git a/src/plugins/spaces_oss/common/types.ts b/src/plugins/spaces_oss/common/types.ts index a7e2257dd424c3..b5c418cf3177e1 100644 --- a/src/plugins/spaces_oss/common/types.ts +++ b/src/plugins/spaces_oss/common/types.ts @@ -6,13 +6,57 @@ * Side Public License, v 1. */ +/** + * A Space. + */ export interface Space { + /** + * The unique identifier for this space. + * The id becomes part of the "URL Identifier" of the space. + * + * Example: an id of `marketing` would result in the URL identifier of `/s/marketing`. + */ id: string; + + /** + * Display name for this space. + */ name: string; + + /** + * Optional description for this space. + */ description?: string; + + /** + * Optional color (hex code) for this space. + * If neither `color` nor `imageUrl` is specified, then a color will be automatically generated. + */ color?: string; + + /** + * Optional display initials for this space's avatar. Supports a maximum of 2 characters. + * If initials are not provided, then they will be derived from the space name automatically. + * + * Initials are not displayed if an `imageUrl` has been specified. + */ initials?: string; + + /** + * Optional base-64 encoded data image url to show as this space's avatar. + * This setting takes precedence over any configured `color` or `initials`. + */ + imageUrl?: string; + + /** + * The set of feature ids that should be hidden within this space. + */ disabledFeatures: string[]; + + /** + * Indicates that this space is reserved (system controlled). + * Reserved spaces cannot be created or deleted by end-users. + * @private + */ _reserved?: boolean; - imageUrl?: string; } diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index 3f562bcbed8e28..bd452c2fca00ec 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -12,30 +12,37 @@ import type { Observable } from 'rxjs'; import type { Space } from '../common'; /** - * @public + * Client-side Spaces API. */ export interface SpacesApi { + /** + * Observable representing the currently active space. + * The details of the space can change without a full page reload (such as display name, color, etc.) + */ readonly activeSpace$: Observable; + + /** + * Retrieve the currently active space. + */ getActiveSpace(): Promise; + /** - * UI API to use to add spaces capabilities to an application + * UI components and services to add spaces capabilities to an application. */ ui: SpacesApiUi; } /** * Function that returns a promise for a lazy-loadable component. - * - * @public */ export type LazyComponentFn = (props: T) => ReactElement; /** - * @public + * UI components and services to add spaces capabilities to an application. */ export interface SpacesApiUi { /** - * Lazy-loadable {@link SpacesApiUiComponent | React components} to support the spaces feature. + * Lazy-loadable {@link SpacesApiUiComponent | React components} to support the Spaces feature. */ components: SpacesApiUiComponent; /** @@ -62,9 +69,7 @@ export interface SpacesApiUi { } /** - * React UI components to be used to display the spaces feature in any application. - * - * @public + * React UI components to be used to display the Spaces feature in any application. */ export interface SpacesApiUiComponent { /** @@ -111,7 +116,7 @@ export interface SpacesApiUiComponent { } /** - * @public + * Properties for the SpacesContext. */ export interface SpacesContextProps { /** @@ -121,7 +126,7 @@ export interface SpacesContextProps { } /** - * @public + * Properties for the ShareToSpaceFlyout. */ export interface ShareToSpaceFlyoutProps { /** @@ -179,7 +184,7 @@ export interface ShareToSpaceFlyoutProps { } /** - * @public + * Describes the target saved object during a share operation. */ export interface ShareToSpaceSavedObjectTarget { /** @@ -215,7 +220,7 @@ export interface ShareToSpaceSavedObjectTarget { } /** - * @public + * Properties for the SpaceList component. */ export interface SpaceListProps { /** @@ -240,7 +245,7 @@ export interface SpaceListProps { } /** - * @public + * Properties for the LegacyUrlConflict component. */ export interface LegacyUrlConflictProps { /** @@ -265,12 +270,18 @@ export interface LegacyUrlConflictProps { } /** - * @public + * Properties for the SpaceAvatar component. */ export interface SpaceAvatarProps { + /** The space to represent with an avatar. */ space: Partial; + + /** The size of the avatar. */ size?: 's' | 'm' | 'l' | 'xl'; + + /** Optional CSS class(es) to apply. */ className?: string; + /** * When enabled, allows EUI to provide an aria-label for this component, which is announced on screen readers. * diff --git a/src/plugins/spaces_oss/public/index.ts b/src/plugins/spaces_oss/public/index.ts index 828ce6f4dec635..9c4d5fd17700c6 100644 --- a/src/plugins/spaces_oss/public/index.ts +++ b/src/plugins/spaces_oss/public/index.ts @@ -8,14 +8,14 @@ import { SpacesOssPlugin } from './plugin'; -export { +export type { SpacesOssPluginSetup, SpacesOssPluginStart, SpacesAvailableStartContract, SpacesUnavailableStartContract, } from './types'; -export { +export type { LazyComponentFn, SpacesApi, SpacesApiUi, diff --git a/src/plugins/spaces_oss/public/types.ts b/src/plugins/spaces_oss/public/types.ts index 22501d3abd8322..df20e9be6eaa1f 100644 --- a/src/plugins/spaces_oss/public/types.ts +++ b/src/plugins/spaces_oss/public/types.ts @@ -8,21 +8,41 @@ import type { SpacesApi } from './api'; +/** + * OSS Spaces plugin start contract when the Spaces feature is enabled. + */ export interface SpacesAvailableStartContract extends SpacesApi { + /** Indicates if the Spaces feature is enabled. */ isSpacesAvailable: true; } +/** + * OSS Spaces plugin start contract when the Spaces feature is disabled. + * @deprecated The Spaces plugin will always be enabled starting in 8.0. + * @removeBy 8.0 + */ export interface SpacesUnavailableStartContract { + /** Indicates if the Spaces feature is enabled. */ isSpacesAvailable: false; } +/** + * OSS Spaces plugin setup contract. + */ export interface SpacesOssPluginSetup { /** * Register a provider for the Spaces API. * * Only one provider can be registered, subsequent calls to this method will fail. + * + * @param provider the API provider. + * + * @private designed to only be consumed by the `spaces` plugin. */ registerSpacesApi(provider: SpacesApi): void; } +/** + * OSS Spaces plugin start contract. + */ export type SpacesOssPluginStart = SpacesAvailableStartContract | SpacesUnavailableStartContract; diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index d31fed4639ffe2..bfa1f79a6487ac 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -215,6 +215,7 @@ export const panel = schema.object({ background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)), drilldown_url: stringOptional, drop_last_bucket: numberIntegerOptional, + ignore_daylight_time: schema.boolean(), filter: schema.maybe(queryObject), gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)), gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])), diff --git a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx index 4ac52a0a80c977..12ffe72135288a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/last_value_mode_indicator.tsx @@ -19,6 +19,7 @@ interface LastValueModeIndicatorProps { seriesData?: PanelData['data']; panelInterval: number; modelInterval: string; + ignoreDaylightTime: boolean; } const lastValueLabel = i18n.translate('visTypeTimeseries.lastValueModeIndicator.lastValue', { @@ -29,6 +30,7 @@ export const LastValueModeIndicator = ({ seriesData, panelInterval, modelInterval, + ignoreDaylightTime, }: LastValueModeIndicatorProps) => { if (!seriesData?.length) return {lastValueLabel}; @@ -40,7 +42,12 @@ export const LastValueModeIndicator = ({ return interval && `${interval.unitValue}${interval.unitString}`; }; - const formatter = createIntervalBasedFormatter(panelInterval, scaledDataFormat, dateFormat); + const formatter = createIntervalBasedFormatter( + panelInterval, + scaledDataFormat, + dateFormat, + ignoreDaylightTime + ); const lastBucketDate = formatter(seriesData[seriesData.length - 1][0]); const formattedPanelInterval = (isAutoInterval(modelInterval) || isGteInterval(modelInterval)) && getFormattedPanelInterval(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts index 562aec31a08032..371a07af7b2e5f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/create_interval_based_formatter.ts @@ -8,6 +8,8 @@ import moment from 'moment'; +const JANUARY_MOMENT_CONFIG = { M: 0, d: 1 }; + function getFormat(interval: number, rules: string[][] = []) { for (let i = rules.length - 1; i >= 0; i--) { const rule = rules[i]; @@ -20,9 +22,15 @@ function getFormat(interval: number, rules: string[][] = []) { export function createIntervalBasedFormatter( interval: number, rules: string[][], - dateFormat: string + dateFormat: string, + ignoreDaylightTime: boolean ) { - return (val: moment.MomentInput): string => { - return moment(val).format(getFormat(interval, rules) ?? dateFormat); + const fixedOffset = moment(JANUARY_MOMENT_CONFIG).utcOffset(); + return (val: moment.MomentInput) => { + const momentVal = moment(val); + if (ignoreDaylightTime) { + momentVal.utcOffset(fixedOffset); + } + return momentVal.format(getFormat(interval, rules) ?? dateFormat); }; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx index 86d3d50eb1f6a6..a2e5ed448c93d6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx @@ -127,6 +127,7 @@ export class TimeseriesPanelConfig extends Component< legend_position: 'right', show_grid: 1, tooltip_mode: 'show_all', + ignore_daylight_time: false, }; const model = { ...defaults, ...this.props.model }; const { selectedTab } = this.state; @@ -227,6 +228,22 @@ export class TimeseriesPanelConfig extends Component< /> + + + + + diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js index 950101103b3a56..d153f318015757 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_config_query_bar_with_ignore_global_filter.js @@ -23,7 +23,7 @@ export function SeriesConfigQueryBarWithIgnoreGlobalFilter({ const htmlId = htmlIdGenerator(); const yesNoOption = ( diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index c15d3e0e4f6f8c..29560c4bd9368b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -33,13 +33,14 @@ class TimeseriesVisualization extends Component { scaledDataFormat = this.props.getConfig('dateFormat:scaled'); dateFormat = this.props.getConfig('dateFormat'); - xAxisFormatter = (interval) => (val) => { + xAxisFormatter = (interval) => { const formatter = createIntervalBasedFormatter( interval, this.scaledDataFormat, - this.dateFormat + this.dateFormat, + this.props.model.ignore_daylight_time ); - return formatter(val); + return (val) => formatter(val); }; yAxisStackedByPercentFormatter = (val) => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/yes_no.tsx b/src/plugins/vis_type_timeseries/public/application/components/yes_no.tsx index 708892bbc681d3..3c02deb177f9ec 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/yes_no.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/yes_no.tsx @@ -13,7 +13,7 @@ import { TimeseriesVisParams } from '../../types'; interface YesNoProps { name: ParamName; - value: TimeseriesVisParams[ParamName]; + value: boolean | number | undefined; disabled?: boolean; 'data-test-subj'?: string; onChange: (partialModel: Partial) => void; diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index e25811fd67d693..c6aba6eccdc9f3 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { combineLatest } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { combineLatest, from } from 'rxjs'; +import { map, tap, switchMap } from 'rxjs/operators'; import { CoreStart, IUiSettingsClient } from 'kibana/public'; +import { getData } from '../services'; import { getSearchParamsFromRequest, SearchRequest, @@ -19,6 +20,25 @@ import { search as dataPluginSearch } from '../../../data/public'; import { VegaInspectorAdapters } from '../vega_inspector'; import { RequestResponder } from '../../../inspector/public'; +const extendSearchParamsWithRuntimeFields = async ( + requestParams: ReturnType, + indexPatternString?: string +) => { + if (indexPatternString) { + const indexPattern = (await getData().indexPatterns.find(indexPatternString)).find( + (index) => index.title === indexPatternString + ); + const runtimeFields = indexPattern?.getComputedFields().runtimeFields; + + return { + ...requestParams, + body: { ...requestParams.body, runtime_mappings: runtimeFields }, + }; + } + + return requestParams; +}; + export interface SearchAPIDependencies { uiSettings: IUiSettingsClient; injectedMetadata: CoreStart['injectedMetadata']; @@ -40,7 +60,7 @@ export class SearchAPI { return combineLatest( searchRequests.map((request) => { const requestId = request.name; - const params = getSearchParamsFromRequest(request, { + const requestParams = getSearchParamsFromRequest(request, { getConfig: this.dependencies.uiSettings.get.bind(this.dependencies.uiSettings), }); @@ -49,18 +69,25 @@ export class SearchAPI { ...request, searchSessionId: this.searchSessionId, }); - requestResponders[requestId].json(params.body); + requestResponders[requestId].json(requestParams.body); } - return search - .search({ params }, { abortSignal: this.abortSignal, sessionId: this.searchSessionId }) - .pipe( - tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), - map((data) => ({ - name: requestId, - rawResponse: data.rawResponse, - })) - ); + return from(extendSearchParamsWithRuntimeFields(requestParams, request.index)).pipe( + switchMap((params) => + search + .search( + { params }, + { abortSignal: this.abortSignal, sessionId: this.searchSessionId } + ) + .pipe( + tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), + map((data) => ({ + name: requestId, + rawResponse: data.rawResponse, + })) + ) + ) + ); }) ); } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 353273d1372e63..b7f2b064cf9c22 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -10,6 +10,7 @@ import $ from 'jquery'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import { scheme, loader, logger, Warn, version as vegaVersion, expressionFunction } from 'vega'; +import { expressionInterpreter } from 'vega-interpreter'; import { version as vegaLiteVersion } from 'vega-lite'; import { Utils } from '../data_model/utils'; import { euiPaletteColorBlind } from '@elastic/eui'; @@ -166,6 +167,7 @@ export class VegaBaseView { createViewConfig() { const config = { + expr: expressionInterpreter, renderer: this._parser.renderer, }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 61ae1ce4e5d783..453e9596a2a4c6 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -183,7 +183,7 @@ export class VegaMapView extends VegaBaseView { protected async _initViewCustomizations() { const vegaView = new View( - parse(injectMapPropsIntoSpec(this._parser.spec)), + parse(injectMapPropsIntoSpec(this._parser.spec), undefined, { ast: true }), this._vegaViewConfig ); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index 5b1e49a73343bf..11b7ca125a4bd4 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -14,7 +14,7 @@ export class VegaView extends VegaBaseView { // In some cases, Vega may be initialized twice... TBD if (!this._$container) return; - const view = new View(parse(this._parser.spec), this._vegaViewConfig); + const view = new View(parse(this._parser.spec, undefined, { ast: true }), this._vegaViewConfig); if (this._parser.useResize) this.updateVegaSize(view); view.initialize(this._$container.get(0), this._$controls.get(0)); diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index 8ca6c6e816aa53..ee50185db8c688 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -96,6 +96,12 @@ export function DataGridProvider({ getService, getPageObjects }: FtrProviderCont })` ); } + + public async getDocCount(): Promise { + const grid = await find.byCssSelector('[data-document-number]'); + return Number(await grid.getAttribute('data-document-number')); + } + public async getFields() { const cells = await find.allByCssSelector('.euiDataGridRowCell'); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 2036ed6c7d3438..e3396d542cb624 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -181,7 +181,6 @@ export class ActionsPlugin implements Plugin( + context: SavedObjectsExportTransformContext, + objects: Array> + ) { + return transformConnectorsForExport(objects, actionTypeRegistry); + }, onImport(connectors) { return { warnings: [ diff --git a/x-pack/plugins/actions/server/saved_objects/transform_connectors_for_export.test.ts b/x-pack/plugins/actions/server/saved_objects/transform_connectors_for_export.test.ts new file mode 100644 index 00000000000000..63fe7c0e32047c --- /dev/null +++ b/x-pack/plugins/actions/server/saved_objects/transform_connectors_for_export.test.ts @@ -0,0 +1,253 @@ +/* + * 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 { transformConnectorsForExport } from './transform_connectors_for_export'; +import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../action_type_registry'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { actionsConfigMock } from '../actions_config.mock'; +import { licensingMock } from '../../../licensing/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { taskManagerMock } from '../../../task_manager/server/mocks'; +import { ActionExecutor, TaskRunnerFactory } from '../lib'; +import { registerBuiltInActionTypes } from '../builtin_action_types'; + +describe('transform connector for export', () => { + const actionTypeRegistryParams: ActionTypeRegistryOpts = { + licensing: licensingMock.createSetup(), + taskManager: taskManagerMock.createSetup(), + taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })), + actionsConfigUtils: actionsConfigMock.create(), + licenseState: licenseStateMock.create(), + preconfiguredActions: [], + }; + const actionTypeRegistry: ActionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + + registerBuiltInActionTypes({ + logger: loggingSystemMock.create().get(), + actionTypeRegistry, + actionsConfigUtils: actionsConfigMock.create(), + }); + + const connectorsWithNoSecrets = [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: '.email', + name: 'email connector without auth', + isMissingSecrets: false, + config: { + hasAuth: false, + from: 'me@me.com', + host: 'host', + port: 22, + service: null, + secure: null, + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: '.index', + name: 'index connector', + isMissingSecrets: false, + config: { + index: 'test-index', + refresh: false, + executionTimeField: null, + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '3', + type: 'action', + attributes: { + actionTypeId: '.server-log', + name: 'server log connector', + isMissingSecrets: false, + config: {}, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '4', + type: 'action', + attributes: { + actionTypeId: '.webhook', + name: 'webhook connector without auth', + isMissingSecrets: false, + config: { + method: 'post', + hasAuth: false, + url: 'https://webhook', + headers: {}, + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + ]; + const connectorsWithSecrets = [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: '.email', + name: 'email connector with auth', + isMissingSecrets: false, + config: { + hasAuth: true, + from: 'me@me.com', + host: 'host', + port: 22, + service: null, + secure: null, + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: '.resilient', + name: 'resilient connector', + isMissingSecrets: false, + config: { + apiUrl: 'https://resilient', + orgId: 'origId', + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '3', + type: 'action', + attributes: { + actionTypeId: '.servicenow', + name: 'servicenow itsm connector', + isMissingSecrets: false, + config: { + apiUrl: 'https://servicenow', + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '4', + type: 'action', + attributes: { + actionTypeId: '.pagerduty', + name: 'pagerduty connector', + isMissingSecrets: false, + config: { + apiUrl: 'https://pagerduty', + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '5', + type: 'action', + attributes: { + actionTypeId: '.jira', + name: 'jira connector', + isMissingSecrets: false, + config: { + apiUrl: 'https://jira', + projectKey: 'foo', + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '6', + type: 'action', + attributes: { + actionTypeId: '.teams', + name: 'teams connector', + isMissingSecrets: false, + config: {}, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '7', + type: 'action', + attributes: { + actionTypeId: '.slack', + name: 'slack connector', + isMissingSecrets: false, + config: {}, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '8', + type: 'action', + attributes: { + actionTypeId: '.servicenow-sir', + name: 'servicenow sir connector', + isMissingSecrets: false, + config: { + apiUrl: 'https://servicenow-sir', + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + { + id: '8', + type: 'action', + attributes: { + actionTypeId: '.webhook', + name: 'webhook connector with auth', + isMissingSecrets: false, + config: { + method: 'post', + hasAuth: true, + url: 'https://webhook', + headers: {}, + }, + secrets: 'asbqw4tqbef', + }, + references: [], + }, + ]; + + it('should not change connectors without secrets', () => { + expect(transformConnectorsForExport(connectorsWithNoSecrets, actionTypeRegistry)).toEqual( + connectorsWithNoSecrets + ); + }); + + it('should remove secrets for connectors with secrets', () => { + expect(transformConnectorsForExport(connectorsWithSecrets, actionTypeRegistry)).toEqual( + connectorsWithSecrets.map((connector) => ({ + ...connector, + attributes: { + ...connector.attributes, + isMissingSecrets: true, + }, + })) + ); + }); +}); diff --git a/x-pack/plugins/actions/server/saved_objects/transform_connectors_for_export.ts b/x-pack/plugins/actions/server/saved_objects/transform_connectors_for_export.ts new file mode 100644 index 00000000000000..050fa20b7fdab2 --- /dev/null +++ b/x-pack/plugins/actions/server/saved_objects/transform_connectors_for_export.ts @@ -0,0 +1,50 @@ +/* + * 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 { SavedObject } from 'kibana/server'; +import { ActionTypeRegistry } from '../action_type_registry'; +import { validateSecrets } from '../lib'; +import { RawAction, ActionType } from '../types'; + +export function transformConnectorsForExport( + connectors: SavedObject[], + actionTypeRegistry: ActionTypeRegistry +): Array> { + return connectors.map((c) => { + const connector = c as SavedObject; + return transformConnectorForExport( + connector, + actionTypeRegistry.get(connector.attributes.actionTypeId) + ); + }); +} + +function transformConnectorForExport( + connector: SavedObject, + actionType: ActionType +): SavedObject { + let isMissingSecrets = false; + try { + // If connector requires secrets, this will throw an error + validateSecrets(actionType, {}); + + // If connector has optional (or no) secrets, set isMissingSecrets value to value of hasAuth + // If connector doesn't have hasAuth value, default to isMissingSecrets: false + isMissingSecrets = (connector?.attributes?.config?.hasAuth as boolean) ?? false; + } catch (err) { + isMissingSecrets = true; + } + + // Skip connectors + return { + ...connector, + attributes: { + ...connector.attributes, + isMissingSecrets, + }, + }; +} diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index bb4383083fedc5..f4a1c0386b54cd 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -5,11 +5,15 @@ * 2.0. */ -import { SavedObjectsServiceSetup } from 'kibana/server'; +import { + SavedObject, + SavedObjectsExportTransformContext, + SavedObjectsServiceSetup, +} from 'kibana/server'; import mappings from './mappings.json'; import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; - +import { transformRulesForExport } from './transform_rule_for_export'; export { partiallyUpdateAlert } from './partially_update_alert'; export const AlertAttributesExcludedFromAAD = [ @@ -43,6 +47,18 @@ export function setupSavedObjects( namespaceType: 'single', migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, + management: { + importableAndExportable: true, + getTitle(obj) { + return `Rule: [${obj.attributes.name}]`; + }, + onExport( + context: SavedObjectsExportTransformContext, + objects: Array> + ) { + return transformRulesForExport(objects); + }, + }, }); savedObjects.registerType({ diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts new file mode 100644 index 00000000000000..bf181e72992209 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformRulesForExport } from './transform_rule_for_export'; + +describe('transform rule for export', () => { + const date = new Date().toISOString(); + const mockRules = [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'rule-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: 'me', + updatedBy: 'me', + createdAt: date, + updatedAt: date, + apiKey: '4tndskbuhewotw4klrhgjewrt9u', + apiKeyOwner: 'me', + throttle: null, + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + scheduledTaskId: '2q5tjbf3q45twer', + }, + references: [], + }, + { + id: '2', + type: 'alert', + attributes: { + enabled: false, + name: 'disabled-rule', + tags: ['tag-1'], + alertTypeId: '456', + consumer: 'alert-consumer', + schedule: { interval: '1h' }, + actions: [], + params: {}, + createdBy: 'you', + updatedBy: 'you', + createdAt: date, + updatedAt: date, + apiKey: null, + apiKeyOwner: null, + throttle: null, + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + scheduledTaskId: null, + }, + references: [], + }, + ]; + + it('should disable rule and clear sensitive values', () => { + expect(transformRulesForExport(mockRules)).toEqual( + mockRules.map((rule) => ({ + ...rule, + attributes: { + ...rule.attributes, + enabled: false, + apiKey: null, + apiKeyOwner: null, + scheduledTaskId: null, + }, + })) + ); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts new file mode 100644 index 00000000000000..c33bbceaf83632 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from 'kibana/server'; +import { RawAlert } from '../types'; + +export function transformRulesForExport(rules: SavedObject[]): Array> { + return rules.map((rule) => transformRuleForExport(rule as SavedObject)); +} + +function transformRuleForExport(rule: SavedObject): SavedObject { + return { + ...rule, + attributes: { + ...rule.attributes, + enabled: false, + apiKey: null, + apiKeyOwner: null, + scheduledTaskId: null, + }, + }; +} diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts index 41ad0e87f14d2f..8434e419ef75a6 100644 --- a/x-pack/plugins/cases/common/api/cases/comment.ts +++ b/x-pack/plugins/cases/common/api/cases/comment.ts @@ -9,6 +9,21 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; +const BucketsAggs = rt.array( + rt.type({ + key: rt.string, + }) +); + +export const GetCaseIdsByAlertIdAggsRt = rt.type({ + references: rt.type({ + doc_count: rt.number, + caseIds: rt.type({ + buckets: BucketsAggs, + }), + }), +}); + /** * this is used to differentiate between an alert attached to a top-level case and a group of alerts that should only * be attached to a sub case. The reason we need this is because an alert group comment will have references to both a case and @@ -123,3 +138,4 @@ export type CommentPatchRequest = rt.TypeOf; export type CommentPatchAttributes = rt.TypeOf; export type CommentRequestUserType = rt.TypeOf; export type CommentRequestAlertType = rt.TypeOf; +export type GetCaseIdsByAlertIdAggs = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index f9fae2466a59ba..966305524c0597 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -31,6 +31,8 @@ export const CASE_STATUS_URL = `${CASES_URL}/status`; export const CASE_TAGS_URL = `${CASES_URL}/tags`; export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; +export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}`; + /** * Action routes */ diff --git a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts new file mode 100644 index 00000000000000..1fc41874fe9d57 --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import Boom from '@hapi/boom'; + +import { RouteDeps } from '../../types'; +import { wrapError } from '../../utils'; +import { CASE_ALERTS_URL } from '../../../../../common/constants'; + +export function initGetCaseIdsByAlertIdApi({ caseService, router, logger }: RouteDeps) { + router.get( + { + path: CASE_ALERTS_URL, + validate: { + params: schema.object({ + alert_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + try { + const alertId = request.params.alert_id; + if (alertId == null || alertId === '') { + throw Boom.badRequest('The `alertId` is not valid'); + } + const client = context.core.savedObjects.client; + const caseIds = await caseService.getCaseIdsByAlertId({ + client, + alertId, + }); + + return response.ok({ + body: caseIds, + }); + } catch (error) { + logger.error( + `Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}` + ); + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index c5b7aa85dc33e0..a1635254dd09b5 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -38,6 +38,7 @@ import { initPatchSubCasesApi } from './cases/sub_case/patch_sub_cases'; import { initFindSubCasesApi } from './cases/sub_case/find_sub_cases'; import { initDeleteSubCasesApi } from './cases/sub_case/delete_sub_cases'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { initGetCaseIdsByAlertIdApi } from './cases/alerts/get_cases'; /** * Default page number when interacting with the saved objects API. @@ -86,4 +87,6 @@ export function initCaseApi(deps: RouteDeps) { initGetCasesStatusApi(deps); // Tags initGetTagsApi(deps); + // Alerts + initGetCaseIdsByAlertIdApi(deps); } diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index a27a8860e96b55..11b8cef6ab5a56 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { AggregationContainer } from '@elastic/elasticsearch/api/types'; import { KibanaRequest, Logger, @@ -17,6 +18,7 @@ import { SavedObjectsBulkResponse, SavedObjectsFindResult, } from 'kibana/server'; +import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; import { @@ -34,6 +36,7 @@ import { CaseResponse, caseTypeField, CasesFindRequest, + GetCaseIdsByAlertIdAggs, } from '../../common'; import { combineFilters, defaultSortField, groupTotalAlertsByID } from '../common'; import { defaultPage, defaultPerPage } from '../routes/api'; @@ -113,6 +116,10 @@ interface GetCommentArgs extends ClientArgs { commentId: string; } +interface GetCaseIdsByAlertIdArgs extends ClientArgs { + alertId: string; +} + interface PostCaseArgs extends ClientArgs { attributes: ESCaseAttributes; } @@ -220,6 +227,7 @@ export interface CaseServiceSetup { getSubCases(args: GetSubCasesArgs): Promise>; getCases(args: GetCasesArgs): Promise>; getComment(args: GetCommentArgs): Promise>; + getCaseIdsByAlertId(args: GetCaseIdsByAlertIdArgs): Promise; getTags(args: ClientArgs): Promise; getReporters(args: ClientArgs): Promise; getUser(args: GetUserArgs): Promise; @@ -899,6 +907,56 @@ export class CaseService implements CaseServiceSetup { } } + private buildCaseIdsAggs = (size: number = 100): Record => ({ + references: { + nested: { + path: `${CASE_COMMENT_SAVED_OBJECT}.references`, + }, + aggregations: { + caseIds: { + terms: { + field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`, + size, + }, + }, + }, + }, + }); + + public async getCaseIdsByAlertId({ + client, + alertId, + }: GetCaseIdsByAlertIdArgs): Promise { + try { + this.log.debug(`Attempting to GET all cases for alert id ${alertId}`); + + let response = await client.find({ + type: CASE_COMMENT_SAVED_OBJECT, + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: this.buildCaseIdsAggs(), + filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, alertId), + }); + if (response.total > 100) { + response = await client.find({ + type: CASE_COMMENT_SAVED_OBJECT, + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: this.buildCaseIdsAggs(response.total), + filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, alertId), + }); + } + return response.aggregations?.references.caseIds.buckets.map((b) => b.key) ?? []; + } catch (error) { + this.log.error(`Error on GET all cases for alert id ${alertId}: ${error}`); + throw error; + } + } + /** * Default behavior is to retrieve all comments that adhere to a given filter (if one is included). * to override this pass in the either the page or perPage options. diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 51eb0bbb1a7e43..d67a297508b143 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -31,6 +31,7 @@ export const createCaseServiceMock = (): CaseServiceMock => ({ getAllSubCaseComments: jest.fn(), getCase: jest.fn(), getCases: jest.fn(), + getCaseIdsByAlertId: jest.fn(), getComment: jest.fn(), getMostRecentSubCase: jest.fn(), getSubCase: jest.fn(), diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index 4a0eaef1e1e9a4..1e7eb9562313d4 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -63,7 +63,6 @@ describe('', () => { }); describe('when there are multiple pages of auto-follow patterns', () => { - let find; let component; let table; let actions; @@ -83,7 +82,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadAutoFollowPatternsResponse({ patterns: autoFollowPatterns }); // Mount the component - ({ find, component, table, actions, form } = setup()); + ({ component, table, actions, form } = setup()); await nextTick(); // Make sure that the http request is fulfilled component.update(); @@ -98,9 +97,8 @@ describe('', () => { expect(tableCellsValues.length).toBe(10); }); - // Skipped until we can figure out how to get this test to work. - test.skip('search works', () => { - form.setInputValue(find('autoFollowPatternSearch'), 'unique'); + test('search works', () => { + form.setInputValue('autoFollowPatternSearch', 'unique'); const { tableCellsValues } = table.getMetaData('autoFollowPatternListTable'); expect(tableCellsValues.length).toBe(1); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index eb85e726d653a8..c2b414ddbd8454 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -69,7 +69,6 @@ describe('', () => { }); describe('when there are multiple pages of follower indices', () => { - let find; let component; let table; let actions; @@ -93,7 +92,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadFollowerIndicesResponse({ indices: followerIndices }); // Mount the component - ({ find, component, table, actions, form } = setup()); + ({ component, table, actions, form } = setup()); await nextTick(); // Make sure that the http request is fulfilled component.update(); @@ -108,9 +107,8 @@ describe('', () => { expect(tableCellsValues.length).toBe(10); }); - // Skipped until we can figure out how to get this test to work. - test.skip('search works', () => { - form.setInputValue(find('followerIndexSearch'), 'unique'); + test('search works', () => { + form.setInputValue('followerIndexSearch', 'unique'); const { tableCellsValues } = table.getMetaData('followerIndexListTable'); expect(tableCellsValues.length).toBe(1); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts index b1725ed8450d5e..e88010e0439a5a 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts @@ -7,3 +7,4 @@ import './breadcrumbs.mock'; import './track_ui_metric.mock'; +import './search_box.mock'; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/search_box.mock.tsx b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/search_box.mock.tsx new file mode 100644 index 00000000000000..f5a51ed044745f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/search_box.mock.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSearchBoxProps } from '@elastic/eui/src/components/search_bar/search_box'; + +jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => { + return { + EuiSearchBox: (props: EuiSearchBoxProps) => ( + ) => { + props.onSearch(event.target.value); + }} + /> + ), + }; +}); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 7ef6be2db72353..87002c936179ae 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -55,11 +55,12 @@ const getFilteredPatterns = (autoFollowPatterns, queryText) => { const normalizedSearchText = queryText.toLowerCase(); return autoFollowPatterns.filter((autoFollowPattern) => { + // default values to avoid undefined errors const { - name, - remoteCluster, - followIndexPatternPrefix, - followIndexPatternSuffix, + name = '', + remoteCluster = '', + followIndexPatternPrefix = '', + followIndexPatternSuffix = '', } = autoFollowPattern; const inName = name.toLowerCase().includes(normalizedSearchText); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 3ccb60a90b2519..f63d55cda1e061 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -54,7 +54,8 @@ const getFilteredIndices = (followerIndices, queryText) => { const normalizedSearchText = queryText.toLowerCase(); return followerIndices.filter((followerIndex) => { - const { name, remoteCluster, leaderIndex } = followerIndex; + // default values to avoid undefined errors + const { name = '', remoteCluster = '', leaderIndex = '' } = followerIndex; if (name.toLowerCase().includes(normalizedSearchText)) { return true; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index 801db2e16ab3b2..bd46724a779342 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { Subject } from 'rxjs'; import { FlyoutCreateDrilldownAction, OpenFlyoutAddDrilldownParams, @@ -22,6 +23,9 @@ const actionParams: OpenFlyoutAddDrilldownParams = { start: () => ({ core: { overlays, + application: { + currentAppId$: new Subject(), + }, } as any, plugins: { uiActionsEnhanced, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 4c0db8f317e516..1d7cab8db14bce 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { skip, take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { Action } from '../../../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { @@ -82,7 +84,11 @@ export class FlyoutCreateDrilldownAction implements Action { } const templates = createDrilldownTemplatesFromSiblings(embeddable); - + const closed$ = new Subject(); + const close = () => { + closed$.next(true); + handle.close(); + }; const handle = core.overlays.openFlyout( toMountPoint( { triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} templates={templates} - onClose={() => handle.close()} + onClose={close} /> ), { @@ -100,5 +106,10 @@ export class FlyoutCreateDrilldownAction implements Action { 'data-test-subj': 'createDrilldownFlyout', } ); + + // Close flyout on application change. + core.application.currentAppId$.pipe(takeUntil(closed$), skip(1), take(1)).subscribe(() => { + close(); + }); } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx index 30ba1b9842c96f..e4c2d5b657630a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { Subject } from 'rxjs'; import { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_edit_drilldown'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; @@ -32,6 +33,9 @@ const actionParams: FlyoutEditDrilldownParams = { start: () => ({ core: { overlays, + application: { + currentAppId$: new Subject(), + }, } as any, plugins: { uiActionsEnhanced: uiActions, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 44eb63bbc504b1..3949b12ace3286 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -6,6 +6,8 @@ */ import React from 'react'; +import { skip, take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { Action } from '../../../../../../../../src/plugins/ui_actions/public'; import { reactToUiComponent, @@ -67,7 +69,11 @@ export class FlyoutEditDrilldownAction implements Action { } const templates = createDrilldownTemplatesFromSiblings(embeddable); - + const closed$ = new Subject(); + const close = () => { + closed$.next(true); + handle.close(); + }; const handle = core.overlays.openFlyout( toMountPoint( { triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} templates={templates} - onClose={() => handle.close()} + onClose={close} /> ), { @@ -84,5 +90,10 @@ export class FlyoutEditDrilldownAction implements Action { 'data-test-subj': 'editDrilldownFlyout', } ); + + // Close flyout on application change. + core.application.currentAppId$.pipe(takeUntil(closed$), skip(1), take(1)).subscribe(() => { + close(); + }); } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.test.tsx index cf30fac3c5f49f..9d1bbafc826622 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.test.tsx @@ -15,6 +15,7 @@ import { EngineCreation } from './'; describe('EngineCreation', () => { const DEFAULT_VALUES = { + isLoading: false, name: '', rawName: '', language: 'Universal', @@ -87,6 +88,15 @@ describe('EngineCreation', () => { false ); }); + + it('passes isLoading state', () => { + setMockValues({ ...DEFAULT_VALUES, isLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find('[data-test-subj="NewEngineSubmitButton"]').prop('isLoading')).toEqual( + true + ); + }); }); describe('EngineCreationNameFormRow', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index 4e1d7bc3e8e482..250c941009ecb9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -40,7 +40,7 @@ import { import { EngineCreationLogic } from './engine_creation_logic'; export const EngineCreation: React.FC = () => { - const { name, rawName, language } = useValues(EngineCreationLogic); + const { name, rawName, language, isLoading } = useValues(EngineCreationLogic); const { setLanguage, setRawName, submitEngine } = useActions(EngineCreationLogic); return ( @@ -104,10 +104,11 @@ export const EngineCreation: React.FC = () => { {ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts index 272e4fb3a25c04..6d5c4480e45f65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.test.ts @@ -23,6 +23,7 @@ describe('EngineCreationLogic', () => { const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { + isLoading: false, name: '', rawName: '', language: 'Universal', @@ -63,6 +64,28 @@ describe('EngineCreationLogic', () => { expect(EngineCreationLogic.values.name).toEqual('name-with-special-characters'); }); }); + + describe('submitEngine', () => { + it('sets isLoading to true', () => { + mount({ isLoading: false }); + EngineCreationLogic.actions.submitEngine(); + expect(EngineCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLoading: true, + }); + }); + }); + + describe('onSubmitError', () => { + it('resets isLoading to false', () => { + mount({ isLoading: true }); + EngineCreationLogic.actions.onSubmitError(); + expect(EngineCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLoading: false, + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts index 6cea32f826e7a3..844cd8fb4088ad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation_logic.ts @@ -22,9 +22,11 @@ interface EngineCreationActions { setLanguage(language: string): { language: string }; setRawName(rawName: string): { rawName: string }; submitEngine(): void; + onSubmitError(): void; } interface EngineCreationValues { + isLoading: boolean; language: string; name: string; rawName: string; @@ -37,8 +39,16 @@ export const EngineCreationLogic = kea ({ language }), setRawName: (rawName) => ({ rawName }), submitEngine: true, + onSubmitError: true, }, reducers: { + isLoading: [ + false, + { + submitEngine: () => true, + onSubmitError: () => false, + }, + ], language: [ DEFAULT_LANGUAGE, { @@ -67,6 +77,7 @@ export const EngineCreationLogic = kea { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index 6b4a081d2c5151..e6a7c03d2aab48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -55,7 +55,6 @@ export const EmptyState: React.FC = () => { actions={ <> { expect(submitButton.prop('disabled')).toEqual(true); }); + + it('passes isLoading state', () => { + setMockValues({ ...DEFAULT_VALUES, isLoading: true }); + const wrapper = shallow(); + const submitButton = wrapper.find('[data-test-subj="NewMetaEngineSubmitButton"]'); + + expect(submitButton.prop('isLoading')).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx index 85c24f1e42368c..02a1768a7528ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx @@ -65,7 +65,7 @@ export const MetaEngineCreation: React.FC = () => { submitEngine, } = useActions(MetaEngineCreationLogic); - const { rawName, name, indexedEngineNames, selectedIndexedEngineNames } = useValues( + const { rawName, name, indexedEngineNames, selectedIndexedEngineNames, isLoading } = useValues( MetaEngineCreationLogic ); @@ -157,10 +157,11 @@ export const MetaEngineCreation: React.FC = () => { selectedIndexedEngineNames.length === 0 || selectedIndexedEngineNames.length > maxEnginesPerMetaEngine } + isLoading={isLoading} type="submit" data-test-subj="NewMetaEngineSubmitButton" - fill color="secondary" + fill > {META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts index 6ffe7034584a16..d8968316d1b720 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts @@ -23,6 +23,7 @@ describe('MetaEngineCreationLogic', () => { const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { + isLoading: false, indexedEngineNames: [], name: '', rawName: '', @@ -76,6 +77,28 @@ describe('MetaEngineCreationLogic', () => { ]); }); }); + + describe('submitEngine', () => { + it('sets isLoading to true', () => { + mount({ isLoading: false }); + MetaEngineCreationLogic.actions.submitEngine(); + expect(MetaEngineCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLoading: true, + }); + }); + }); + + describe('onSubmitError', () => { + it('resets isLoading to false', () => { + mount({ isLoading: true }); + MetaEngineCreationLogic.actions.onSubmitError(); + expect(MetaEngineCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLoading: false, + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts index d94eb82a49c0eb..472a0dee12b7fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts @@ -25,6 +25,7 @@ interface MetaEngineCreationValues { name: string; rawName: string; selectedIndexedEngineNames: string[]; + isLoading: boolean; } interface MetaEngineCreationActions { @@ -38,6 +39,7 @@ interface MetaEngineCreationActions { selectedIndexedEngineNames: MetaEngineCreationValues['selectedIndexedEngineNames'] ): { selectedIndexedEngineNames: MetaEngineCreationValues['selectedIndexedEngineNames'] }; submitEngine(): void; + onSubmitError(): void; } export const MetaEngineCreationLogic = kea< @@ -50,9 +52,17 @@ export const MetaEngineCreationLogic = kea< setIndexedEngineNames: (indexedEngineNames) => ({ indexedEngineNames }), setRawName: (rawName) => ({ rawName }), setSelectedIndexedEngineNames: (selectedIndexedEngineNames) => ({ selectedIndexedEngineNames }), - submitEngine: () => null, + submitEngine: true, + onSubmitError: true, }, reducers: { + isLoading: [ + false, + { + submitEngine: () => true, + onSubmitError: () => false, + }, + ], indexedEngineNames: [ [], { @@ -121,6 +131,7 @@ export const MetaEngineCreationLogic = kea< actions.onEngineCreationSuccess(); } catch (e) { flashAPIErrors(e); + actions.onSubmitError(); } }, }), diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index a9526d9450993d..3c9cf01bbb4a96 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -293,13 +293,13 @@ describe('RoleMappingsLogic', () => { expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings', { body: JSON.stringify({ + roleType: 'admin', + allGroups: false, + authProvider: [ANY_AUTH_PROVIDER], rules: { username: '', }, - roleType: 'admin', groups: [], - allGroups: false, - authProvider: [ANY_AUTH_PROVIDER], }), }); await nextTick(); @@ -317,13 +317,13 @@ describe('RoleMappingsLogic', () => { `/api/workplace_search/org/role_mappings/${wsRoleMapping.id}`, { body: JSON.stringify({ + roleType: 'admin', + allGroups: true, + authProvider: [ANY_AUTH_PROVIDER, 'other_auth'], rules: { username: 'user', }, - roleType: 'admin', groups: [], - allGroups: true, - authProvider: [ANY_AUTH_PROVIDER, 'other_auth'], }), } ); 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 6e3b74f95f7076..0df7f0ba375698 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 @@ -28,8 +28,8 @@ import { } from './constants'; interface RoleMappingsServerDetails { - multipleAuthProvidersConfig: boolean; roleMappings: WSRoleMapping[]; + multipleAuthProvidersConfig: boolean; } interface RoleMappingServerDetails { @@ -41,68 +41,69 @@ interface RoleMappingServerDetails { roleMapping?: WSRoleMapping; } +const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName => + Object.entries(roleMapping.rules)[0][0] as AttributeName; +const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => + Object.entries(roleMapping.rules)[0][1] as string; + interface RoleMappingsActions { - setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; - setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails; - handleRoleChange(roleType: Role): { roleType: Role }; handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; + handleAuthProviderChange(value: string[]): { value: string[] }; handleAttributeSelectorChange( value: AttributeName, firstElasticsearchRole: string ): { value: AttributeName; firstElasticsearchRole: string }; handleAttributeValueChange(value: string): { value: string }; + handleDeleteMapping(): void; handleGroupSelectionChange( groupId: string, selected: boolean ): { groupId: string; selected: boolean }; - handleAuthProviderChange(value: string[]): { value: string[] }; - resetState(): void; - initializeRoleMapping(roleId?: string): { roleId?: string }; + handleRoleChange(roleType: Role): { roleType: Role }; handleSaveMapping(): void; - handleDeleteMapping(): void; + initializeRoleMapping(roleId?: string): { roleId?: string }; initializeRoleMappings(): void; + resetState(): void; + setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails; + setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; } interface RoleMappingsValues { + includeInAllGroups: boolean; + attributeName: AttributeName; + attributeValue: string; attributes: string[]; availableAuthProviders: string[]; + availableGroups: RoleGroup[]; + dataLoading: boolean; elasticsearchRoles: string[]; + multipleAuthProvidersConfig: boolean; roleMapping: WSRoleMapping | null; roleMappings: WSRoleMapping[]; roleType: Role; - attributeValue: string; - attributeName: AttributeName; - dataLoading: boolean; - multipleAuthProvidersConfig: boolean; - availableGroups: RoleGroup[]; - selectedGroups: Set; - includeInAllGroups: boolean; selectedAuthProviders: string[]; + selectedGroups: Set; } -const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName => - Object.entries(roleMapping.rules)[0][0] as AttributeName; -const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => - Object.entries(roleMapping.rules)[0][1] as string; - export const RoleMappingsLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'role_mappings'], actions: { setRoleMappingsData: (data: RoleMappingsServerDetails) => data, setRoleMappingData: (data: RoleMappingServerDetails) => data, + handleAuthProviderChange: (value: string[]) => ({ value }), handleRoleChange: (roleType: Role) => ({ roleType }), handleGroupSelectionChange: (groupId: string, selected: boolean) => ({ groupId, selected }), - handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }), handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ value, firstElasticsearchRole, }), handleAttributeValueChange: (value: string) => ({ value }), - handleAuthProviderChange: (value: string[]) => ({ value }), - resetState: () => true, + handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }), + resetState: true, + initializeRoleMappings: true, initializeRoleMapping: (roleId?: string) => ({ roleId }), - handleSaveMapping: () => true, - handleDeleteMapping: () => true, - initializeRoleMappings: () => true, + handleDeleteMapping: true, + handleSaveMapping: true, }, reducers: { dataLoading: [ @@ -120,10 +121,12 @@ export const RoleMappingsLogic = kea [], }, ], - attributes: [ - [], + multipleAuthProvidersConfig: [ + false, { - setRoleMappingData: (_, { attributes }) => attributes, + setRoleMappingsData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + resetState: () => false, }, ], availableGroups: [ @@ -132,33 +135,10 @@ export const RoleMappingsLogic = kea availableGroups, }, ], - selectedGroups: [ - new Set(), - { - setRoleMappingData: (_, { roleMapping, availableGroups }) => - roleMapping - ? new Set(roleMapping.groups.map((group) => group.id)) - : new Set( - availableGroups - .filter((group) => group.name === DEFAULT_GROUP_NAME) - .map((group) => group.id) - ), - handleGroupSelectionChange: (groups, { groupId, selected }) => { - const newSelectedGroupNames = new Set(groups as Set); - if (selected) { - newSelectedGroupNames.add(groupId); - } else { - newSelectedGroupNames.delete(groupId); - } - return newSelectedGroupNames; - }, - }, - ], - includeInAllGroups: [ - false, + attributes: [ + [], { - setRoleMappingData: (_, { roleMapping }) => (roleMapping ? roleMapping.allGroups : false), - handleAllGroupsSelectionChange: (_, { selected }) => selected, + setRoleMappingData: (_, { attributes }) => attributes, }, ], elasticsearchRoles: [ @@ -182,6 +162,13 @@ export const RoleMappingsLogic = kea roleType, }, ], + includeInAllGroups: [ + false, + { + setRoleMappingData: (_, { roleMapping }) => (roleMapping ? roleMapping.allGroups : false), + handleAllGroupsSelectionChange: (_, { selected }) => selected, + }, + ], attributeValue: [ '', { @@ -202,18 +189,32 @@ export const RoleMappingsLogic = kea 'username', }, ], - availableAuthProviders: [ - [], + selectedGroups: [ + new Set(), { - setRoleMappingData: (_, { authProviders }) => authProviders, + setRoleMappingData: (_, { roleMapping, availableGroups }) => + roleMapping + ? new Set(roleMapping.groups.map((group) => group.id)) + : new Set( + availableGroups + .filter((group) => group.name === DEFAULT_GROUP_NAME) + .map((group) => group.id) + ), + handleGroupSelectionChange: (groups, { groupId, selected }) => { + const newSelectedGroupNames = new Set(groups as Set); + if (selected) { + newSelectedGroupNames.add(groupId); + } else { + newSelectedGroupNames.delete(groupId); + } + return newSelectedGroupNames; + }, }, ], - multipleAuthProvidersConfig: [ - false, + availableAuthProviders: [ + [], { - setRoleMappingsData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, - setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, - resetState: () => false, + setRoleMappingData: (_, { authProviders }) => authProviders, }, ], selectedAuthProviders: [ @@ -264,13 +265,13 @@ export const RoleMappingsLogic = kea { + const { roleMapping } = values; + if (!roleMapping) return; + const { http } = HttpLogic.values; const { navigateToUrl } = KibanaLogic.values; - const { roleMapping } = values; - if (!roleMapping) { - return; - } const route = `/api/workplace_search/org/role_mappings/${roleMapping.id}`; + if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { try { await http.delete(route); @@ -295,13 +296,13 @@ export const RoleMappingsLogic = kea { }); }); - describe('when the spaces plugin is unavailable', () => { + describe('when the Spaces plugin is unavailable', () => { describe('when security is disabled', () => { it('should allow all access', async () => { const spaces = undefined; diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 0de03e54e1f790..ba809187a549e0 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -27,6 +27,20 @@ describe('Features Plugin', () => { namespaceType: 'single' as 'single', }, ]); + typeRegistry.getImportableAndExportableTypes.mockReturnValue([ + { + name: 'hidden-importableAndExportable', + hidden: true, + mappings: { properties: {} }, + namespaceType: 'single' as 'single', + }, + { + name: 'not-hidden-importableAndExportable', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'single' as 'single', + }, + ]); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); }); @@ -87,7 +101,9 @@ describe('Features Plugin', () => { `); }); - it('registers kibana features with not hidden saved objects types', async () => { + it('registers kibana features with visible saved objects types and hidden saved object types that are importable and exportable', async () => { + typeRegistry.isHidden.mockReturnValueOnce(true); + typeRegistry.isHidden.mockReturnValueOnce(false); const plugin = new FeaturesPlugin(initContext); await plugin.setup(coreSetup, {}); const { getKibanaFeatures } = plugin.start(coreStart); @@ -98,6 +114,8 @@ describe('Features Plugin', () => { expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('bar')).toBe(false); + expect(soTypes.includes('hidden-importableAndExportable')).toBe(true); + expect(soTypes.includes('not-hidden-importableAndExportable')).toBe(false); }); it('returns registered elasticsearch features', async () => { diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 09a5b78ad868a0..60a48a539f81ec 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -128,7 +128,15 @@ export class FeaturesPlugin private registerOssFeatures(savedObjects: SavedObjectsServiceStart) { const registry = savedObjects.getTypeRegistry(); - const savedObjectTypes = registry.getVisibleTypes().map((t) => t.name); + const savedObjectVisibleTypes = registry.getVisibleTypes().map((t) => t.name); + const savedObjectImportableAndExportableHiddenTypes = registry + .getImportableAndExportableTypes() + .filter((t) => registry.isHidden(t.name)) + .map((t) => t.name); + + const savedObjectTypes = Array.from( + new Set([...savedObjectVisibleTypes, ...savedObjectImportableAndExportableHiddenTypes]) + ); this.logger.debug( `Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${ diff --git a/x-pack/plugins/file_upload/server/routes.ts b/x-pack/plugins/file_upload/server/routes.ts index 8e6651ed891c6d..847a57afb391cc 100644 --- a/x-pack/plugins/file_upload/server/routes.ts +++ b/x-pack/plugins/file_upload/server/routes.ts @@ -185,7 +185,7 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge /** * @apiGroup FileDataVisualizer * - * @api {post} /internal/file_upload/index_exists ES Field caps wrapper checks if index exists + * @api {post} /internal/file_upload/index_exists ES indices exists wrapper checks if index exists * @apiName IndexExists */ router.post( @@ -200,20 +200,10 @@ export function fileUploadRoutes(coreSetup: CoreSetup, logge }, async (context, request, response) => { try { - const { index } = request.body; - - const options = { - index: [index], - fields: ['*'], - ignore_unavailable: true, - allow_no_indices: true, - }; - - const { body } = await context.core.elasticsearch.client.asCurrentUser.fieldCaps(options); - const exists = Array.isArray(body.indices) && body.indices.length !== 0; - return response.ok({ - body: { exists }, - }); + const { + body: indexExists, + } = await context.core.elasticsearch.client.asCurrentUser.indices.exists(request.body); + return response.ok({ body: { exists: indexExists } }); } catch (e) { return response.customError(wrapError(e)); } diff --git a/x-pack/plugins/ml/common/types/common.ts b/x-pack/plugins/ml/common/types/common.ts index a5253456252cd5..fdeffe14ddaf71 100644 --- a/x-pack/plugins/ml/common/types/common.ts +++ b/x-pack/plugins/ml/common/types/common.ts @@ -46,3 +46,7 @@ export interface ListingPageUrlState { export type AppPageState = { [key in MlPages]?: Partial; }; + +type Without = { [P in Exclude]?: never }; + +export type XOR = T | U extends object ? (Without & U) | (Without & T) : T | U; diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index 6b320d503b4c02..3c4c3af7486457 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -7,6 +7,7 @@ import { DataFrameAnalyticsConfig } from './data_frame_analytics'; import { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance'; +import { XOR } from './common'; export interface IngestStats { count: number; @@ -45,22 +46,54 @@ export interface TrainedModelStat { }; } +type TreeNode = object; + +export type PutTrainedModelConfig = { + description?: string; + metadata?: { + analytics_config: DataFrameAnalyticsConfig; + input: unknown; + total_feature_importance?: TotalFeatureImportance[]; + feature_importance_baseline?: FeatureImportanceBaseline; + model_aliases?: string[]; + } & Record; + tags?: string[]; + inference_config?: Record; + input: { field_names: string[] }; +} & XOR< + { compressed_definition: string }, + { + definition: { + preprocessors: object[]; + trained_model: { + tree: { + classification_labels?: string; + feature_names: string; + target_type: string; + tree_structure: TreeNode[]; + }; + tree_node: TreeNode; + ensemble?: object; + }; + }; + } +>; // compressed_definition and definition are mutually exclusive + export interface TrainedModelConfigResponse { - description: string; + description?: string; created_by: string; create_time: string; default_field_map: Record; estimated_heap_memory_usage_bytes: number; estimated_operations: number; license_level: string; - metadata?: - | { - analytics_config: DataFrameAnalyticsConfig; - input: any; - total_feature_importance?: TotalFeatureImportance[]; - feature_importance_baseline?: FeatureImportanceBaseline; - } - | Record; + metadata?: { + analytics_config: DataFrameAnalyticsConfig; + input: unknown; + total_feature_importance?: TotalFeatureImportance[]; + feature_importance_baseline?: FeatureImportanceBaseline; + model_aliases?: string[]; + } & Record; model_id: string; tags: string[]; version: string; diff --git a/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx b/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx index 5064cf64352273..93276067f96633 100644 --- a/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx +++ b/x-pack/plugins/ml/public/application/components/vega_chart/vega_chart_view.tsx @@ -15,6 +15,7 @@ import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; // @ts-ignore import { compile } from 'vega-lite/build/vega-lite'; import { parse, View, Warn } from 'vega'; +import { expressionInterpreter } from 'vega-interpreter'; import { Handler } from 'vega-tooltip'; import { htmlIdGenerator } from '@elastic/eui'; @@ -29,7 +30,7 @@ export const VegaChartView: FC = ({ vegaSpec }) => { useEffect(() => { const vgSpec = compile(vegaSpec).spec; - const view = new View(parse(vgSpec)) + const view = new View(parse(vgSpec, undefined, { ast: true }), { expr: expressionInterpreter }) .logLevel(Warn) .renderer('canvas') .tooltip(new Handler().call) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index 32aa14559da0e3..d26b5d5cfc16ff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -31,6 +31,7 @@ export const AnalyticsNavigationBar: FC<{ defaultMessage: 'Jobs', }), path: '/data_frame_analytics', + testSubj: 'mlAnalyticsJobsTab', }, { id: 'models', @@ -38,6 +39,7 @@ export const AnalyticsNavigationBar: FC<{ defaultMessage: 'Models', }), path: '/data_frame_analytics/models', + testSubj: 'mlTrainedModelsTab', }, ]; if (jobId !== undefined || modelId !== undefined) { @@ -47,6 +49,7 @@ export const AnalyticsNavigationBar: FC<{ defaultMessage: 'Map', }), path: '/data_frame_analytics/map', + testSubj: '', }); } return navTabs; @@ -67,6 +70,7 @@ export const AnalyticsNavigationBar: FC<{ key={`tab-${tab.id}`} isSelected={tab.id === selectedTabId} onClick={onTabClick.bind(null, tab)} + data-test-subj={tab.testSubj} > {tab.name} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx index 476931e4b85515..88ffaa0da7fdcd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx @@ -29,6 +29,7 @@ import { ModelItemFull } from './models_list'; import { useMlKibana } from '../../../../../contexts/kibana'; import { timeFormatter } from '../../../../../../../common/util/date_utils'; import { isDefined } from '../../../../../../../common/types/guards'; +import { isPopulatedObject } from '../../../../../../../common'; interface ExpandedRowProps { item: ModelItemFull; @@ -70,6 +71,8 @@ export const ExpandedRow: FC = ({ item }) => { description, } = item; + const { analytics_config: analyticsConfig, ...restMetaData } = metadata ?? {}; + const details = { description, tags, @@ -148,6 +151,26 @@ export const ExpandedRow: FC = ({ item }) => { /> + {isPopulatedObject(restMetaData) ? ( + + + +
+ +
+
+ + +
+
+ ) : null} ), @@ -186,7 +209,7 @@ export const ExpandedRow: FC = ({ item }) => { /> - {metadata?.analytics_config && ( + {analyticsConfig && ( @@ -201,7 +224,7 @@ export const ExpandedRow: FC = ({ item }) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 79b8c7130e73cb..b9803f1ea26e01 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -292,7 +292,7 @@ export const ModelsList: FC = () => { }), icon: 'visTable', type: 'icon', - available: (item) => item.metadata?.analytics_config?.id, + available: (item) => !!item.metadata?.analytics_config?.id, onClick: async (item) => { if (item.metadata?.analytics_config === undefined) return; @@ -327,7 +327,7 @@ export const ModelsList: FC = () => { icon: 'graphApp', type: 'icon', isPrimary: true, - available: (item) => item.metadata?.analytics_config?.id, + available: (item) => !!item.metadata?.analytics_config?.id, onClick: async (item) => { const path = await mlUrlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_MAP, diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx new file mode 100644 index 00000000000000..fcdd0a71a8f55e --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -0,0 +1,268 @@ +/* + * 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, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiAccordion, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiTitle, + htmlIdGenerator, +} from '@elastic/eui'; +import { VectorLayerDescriptor } from '../../../../maps/common/descriptor_types'; +import { + FIELD_ORIGIN, + SOURCE_TYPES, + STYLE_TYPE, + COLOR_MAP_TYPE, +} from '../../../../maps/common/constants'; +import { useMlKibana } from '../contexts/kibana'; +import { MlEmbeddedMapComponent } from '../components/ml_embedded_map'; +import { EMSTermJoinConfig } from '../../../../maps/public'; +import { AnomaliesTableRecord } from '../../../common/types/anomalies'; + +const MAX_ENTITY_VALUES = 3; +const COMMON_EMS_LAYER_IDS = [ + 'world_countries', + 'administrative_regions_lvl2', + 'usa_zip_codes', + 'usa_states', +]; + +function getAnomalyRows(anomalies: AnomaliesTableRecord[], jobId: string) { + const anomalyRows: Record< + string, + { count: number; entityValue: string; max_severity: number } + > = {}; + for (let i = 0; i < anomalies.length; i++) { + const anomaly = anomalies[i]; + const location = anomaly.entityValue; + if (anomaly.jobId !== jobId) continue; + + if (anomalyRows[location] === undefined) { + // add it to the object and set count to 1 + anomalyRows[location] = { + count: 1, + entityValue: location, + max_severity: Math.floor(anomaly.severity), + }; + } else { + anomalyRows[location].count += 1; + if (anomaly.severity > anomalyRows[location].max_severity) { + anomalyRows[location].max_severity = Math.floor(anomaly.severity); + } + } + } + return Object.values(anomalyRows); +} + +export const getChoroplethAnomaliesLayer = ( + anomalies: AnomaliesTableRecord[], + { layerId, field, jobId }: MLEMSTermJoinConfig, + visible: boolean +): VectorLayerDescriptor => { + return { + id: htmlIdGenerator()(), + label: i18n.translate('xpack.ml.explorer.anomaliesMap.anomaliesCount', { + defaultMessage: 'Anomalies count: {jobId}', + values: { jobId }, + }), + joins: [ + { + // Left join is the id from the type of field (e.g. world_countries) + leftField: field, + right: { + id: 'anomaly_count', + type: SOURCE_TYPES.TABLE_SOURCE, + __rows: getAnomalyRows(anomalies, jobId), + __columns: [ + { + name: 'entityValue', + type: 'string', + }, + { + name: 'count', + type: 'number', + }, + { + name: 'max_severity', + type: 'number', + }, + ], + // Right join/term is the field in the doc you’re trying to join it to (foreign key - e.g. US) + term: 'entityValue', + }, + }, + ], + sourceDescriptor: { + type: 'EMS_FILE', + id: layerId, + }, + style: { + type: 'VECTOR', + // @ts-ignore missing style properties. Remove once 'VectorLayerDescriptor' type is updated + properties: { + icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + color: 'Blue to Red', + colorCategory: 'palette_0', + fieldMetaOptions: { isEnabled: true, sigma: 3 }, + type: COLOR_MAP_TYPE.ORDINAL, + field: { + name: 'count', + origin: FIELD_ORIGIN.JOIN, + }, + useCustomColorRamp: false, + }, + }, + lineColor: { + type: STYLE_TYPE.DYNAMIC, + options: { fieldMetaOptions: { isEnabled: true } }, + }, + lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, + }, + isTimeAware: true, + }, + visible, + type: 'VECTOR', + }; +}; + +interface Props { + anomalies: AnomaliesTableRecord[]; + jobIds: string[]; +} + +interface MLEMSTermJoinConfig extends EMSTermJoinConfig { + jobId: string; +} + +export const AnomaliesMap: FC = ({ anomalies, jobIds }) => { + const [EMSSuggestions, setEMSSuggestions] = useState< + Array | undefined + >(); + const { + services: { maps: mapsPlugin }, + } = useMlKibana(); + + const getEMSTermSuggestions = useCallback(async (): Promise => { + if (!mapsPlugin) return; + + const suggestions = await Promise.all( + jobIds.map(async (jobId) => { + const entityValues = new Set(); + let entityName; + for (let i = 0; i < anomalies.length; i++) { + if ( + jobId === anomalies[i].jobId && + anomalies[i].entityValue !== '' && + anomalies[i].entityValue !== undefined && + anomalies[i].entityName !== '' && + anomalies[i].entityName !== undefined + ) { + entityValues.add(anomalies[i].entityValue); + + if (!entityName) { + entityName = anomalies[i].entityName; + } + } + + if ( + // convert to set so it's unique values + entityValues.size === MAX_ENTITY_VALUES + ) + break; + } + + const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ + emsLayerIds: COMMON_EMS_LAYER_IDS, + sampleValues: Array.from(entityValues), + sampleValuesColumnName: entityName || '', + }); + if (suggestion) { + return { jobId, ...suggestion }; + } + return suggestion; + }) + ); + + setEMSSuggestions(suggestions.filter((s) => s !== null)); + }, [...jobIds]); + + useEffect( + function getInitialEMSTermSuggestions() { + if (anomalies && anomalies.length > 0) { + getEMSTermSuggestions(); + } + }, + [...jobIds] + ); + + const layerList = useMemo(() => { + let layers: VectorLayerDescriptor[] = []; + // Loop through suggestions list and make a layer for each + if (EMSSuggestions?.length) { + let count = 0; + layers = EMSSuggestions.reduce(function (result, suggestion) { + if (suggestion) { + const visible = count === 0; + result.push(getChoroplethAnomaliesLayer(anomalies, suggestion, visible)); + count++; + } + return result; + }, [] as VectorLayerDescriptor[]); + } + return layers; + }, [EMSSuggestions, anomalies]); + + if (EMSSuggestions?.length === 0) { + return null; + } + + return ( + <> + + +

+ + ), + }} + /> +

+ + } + > +
+ +
+
+
+ + + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 47d3e154ad3257..7cc1d0d86e2ff2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -70,6 +70,9 @@ import { ExplorerChartsContainer } from './explorer_charts/explorer_charts_conta // Anomalies Table import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; +// Anomalies Map +import { AnomaliesMap } from './anomalies_map'; + import { getToastNotifications } from '../util/dependency_cache'; import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; @@ -399,6 +402,9 @@ export class ExplorerUI extends React.Component { )} + {loading === false && tableData.anomalies?.length && ( + + )} {annotationsData.length > 0 && ( <> diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts index 8bfece0e4c732e..49d20d3f59b616 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/map_config.ts @@ -7,6 +7,7 @@ import { FIELD_ORIGIN, STYLE_TYPE } from '../../../../../maps/common/constants'; import { ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../../../../common'; +import { AnomaliesTableData } from '../explorer_utils'; const FEATURE = 'Feature'; const POINT = 'Point'; @@ -29,7 +30,10 @@ const SEVERITY_COLOR_RAMP = [ }, ]; -function getAnomalyFeatures(anomalies: any[], type: 'actual_point' | 'typical_point') { +function getAnomalyFeatures( + anomalies: AnomaliesTableData['anomalies'], + type: 'actual_point' | 'typical_point' +) { const anomalyFeatures = []; for (let i = 0; i < anomalies.length; i++) { const anomaly = anomalies[i]; @@ -58,7 +62,7 @@ function getAnomalyFeatures(anomalies: any[], type: 'actual_point' | 'typical_po return anomalyFeatures; } -export const getMLAnomaliesTypicalLayer = (anomalies: any) => { +export const getMLAnomaliesTypicalLayer = (anomalies: AnomaliesTableData['anomalies']) => { return { id: 'anomalies_typical_layer', label: 'Typical', diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts index bafa5c300e79f8..84f0fbaea05791 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/models_provider.ts @@ -11,8 +11,8 @@ import { PipelineDefinition } from '../../../common/types/trained_models'; export function modelsProvider(client: IScopedClusterClient) { return { /** - * Retrieves the map of model ids and associated pipelines. - * @param modelIds + * Retrieves the map of model ids and aliases with associated pipelines. + * @param modelIds - Array of models ids and model aliases. */ async getModelsPipelines(modelIds: string[]) { const modelIdsMap = new Map | null>( diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index dbfc2195a12e1e..c4b2d63b05d13e 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -13,6 +13,7 @@ import { optionalModelIdSchema, } from './schemas/inference_schema'; import { modelsProvider } from '../models/data_frame_analytics'; +import { TrainedModelConfigResponse } from '../../common/types/trained_models'; export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) { /** @@ -42,14 +43,32 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ...query, ...(modelId ? { model_id: modelId } : {}), }); - const result = body.trained_model_configs; + const result = body.trained_model_configs as TrainedModelConfigResponse[]; try { if (withPipelines) { + const modelIdsAndAliases: string[] = Array.from( + new Set( + result + .map(({ model_id: id, metadata }) => { + return [id, ...(metadata?.model_aliases ?? [])]; + }) + .flat() + ) + ); + const pipelinesResponse = await modelsProvider(client).getModelsPipelines( - result.map(({ model_id: id }: { model_id: string }) => id) + modelIdsAndAliases ); for (const model of result) { - model.pipelines = pipelinesResponse.get(model.model_id)!; + model.pipelines = { + ...(pipelinesResponse.get(model.model_id) ?? {}), + ...(model.metadata?.model_aliases ?? []).reduce((acc, alias) => { + return { + ...acc, + ...(pipelinesResponse.get(alias) ?? {}), + }; + }, {}), + }; } } } catch (e) { diff --git a/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx b/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx index dcfd3adf721685..1ec447ee2f84a1 100644 --- a/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx +++ b/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx @@ -65,6 +65,7 @@ const getHeading = (isFleetTypeMetric: boolean) => { defaultMessage="APM & Fleet Server" /> ); + return titles; } titles.title = i18n.translate('xpack.monitoring.apm.metrics.topCharts.title', { defaultMessage: 'APM Server - Resource Usage', diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index 64afe8988e8bf0..6ab6ed758c050b 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -50,6 +50,7 @@ const getServerTitle = (isFleetTypeMetric, total) => { values: { apmsTotal }, } ); + return linkLabel; } linkLabel.link = ( { if (!Legacy.shims.isCloud || !versions) { return false; } + let criteriaPassed = false; versions.forEach((version) => { const [major, minor] = version.split('.'); const majorInt = Number(major); if (majorInt > 7 || (majorInt === 7 && Number(minor) >= 13)) { - return true; + criteriaPassed = true; + return; } }); - return false; + return criteriaPassed; }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js index e002652aa3aeff..c91732019f79fe 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import { act } from 'react-dom/test-utils'; import { getRouter } from '../../../public/application/services'; @@ -16,6 +17,19 @@ import { setupEnvironment, getRandomString, findTestSubject } from '../helpers'; import { setup } from './remote_clusters_list.helpers'; +jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => { + return { + EuiSearchBox: (props) => ( + { + props.onSearch(event.target.value); + }} + /> + ), + }; +}); + describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -64,7 +78,6 @@ describe('', () => { }); describe('when there are multiple pages of remote clusters', () => { - let find; let table; let actions; let component; @@ -88,7 +101,7 @@ describe('', () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); await act(async () => { - ({ find, table, actions, form, component } = setup()); + ({ table, actions, component, form } = setup()); }); component.update(); @@ -103,9 +116,8 @@ describe('', () => { expect(tableCellsValues.length).toBe(10); }); - // Skipped until we can figure out how to get this test to work. - test.skip('search works', () => { - form.setInputValue(find('remoteClusterSearch'), 'unique'); + test('search works', () => { + form.setInputValue('remoteClusterSearch', 'unique'); const { tableCellsValues } = table.getMetaData('remoteClusterListTable'); expect(tableCellsValues.length).toBe(1); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 221e0ec245fd40..2ac99d4f63fbac 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiButtonIcon, EuiTextArea } from '@elastic/eui'; +import { EuiButtonIcon, EuiComboBox, EuiTextArea } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; +import { findTestSubject, mountWithIntl, nextTick, shallowWithIntl } from '@kbn/test/jest'; +import { indicesAPIClientMock } from '../../../index.mock'; import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; @@ -25,7 +26,7 @@ test('it renders without crashing', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -52,7 +53,7 @@ test('it allows for custom index privileges', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -86,7 +87,7 @@ describe('delete button', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -138,7 +139,7 @@ describe(`document level security`, () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -197,7 +198,7 @@ describe('field level security', () => { }, formIndex: 0, indexPatterns: [], - availableFields: [], + indicesAPIClient: indicesAPIClientMock.create(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], isRoleReadOnly: false, allowDocumentLevelSecurity: true, @@ -208,19 +209,21 @@ describe('field level security', () => { intl: {} as any, }; - test(`inputs are hidden when FLS is not allowed`, () => { + test(`inputs are hidden when FLS is not allowed, and fields are not queried`, async () => { const testProps = { ...props, allowFieldLevelSecurity: false, }; const wrapper = mountWithIntl(); + await nextTick(); expect(wrapper.find('EuiSwitch[data-test-subj="restrictFieldsQuery0"]')).toHaveLength(0); expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); expect(wrapper.find('.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); }); - test('only the switch is shown when allowed, and FLS is empty', () => { + test('renders the FLS switch when available, but collapsed when no fields are selected', async () => { const testProps = { ...props, indexPrivilege: { @@ -230,19 +233,126 @@ describe('field level security', () => { }; const wrapper = mountWithIntl(); + await nextTick(); expect(wrapper.find('EuiSwitch[data-test-subj="restrictFieldsQuery0"]')).toHaveLength(1); expect(wrapper.find('.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); expect(wrapper.find('.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); }); - test('inputs are shown when allowed', () => { + test('FLS inputs are shown when allowed', async () => { const testProps = { ...props, }; const wrapper = mountWithIntl(); + await nextTick(); expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); + }); + + test('does not query for available fields when a request is already in flight', async () => { + jest.useFakeTimers(); + + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indicesAPIClient: indicesAPIClientMock.create(), + }; + + testProps.indicesAPIClient.getFields.mockImplementation(async () => { + return new Promise((resolve) => + setTimeout(() => { + resolve(['foo']); + }, 5000) + ); + }); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + + findTestSubject(wrapper, 'fieldInput0').simulate('focus'); + jest.advanceTimersByTime(2000); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + + findTestSubject(wrapper, 'fieldInput0').simulate('focus'); + jest.advanceTimersByTime(4000); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + }); + + test('queries for available fields when mounted, and FLS is available', async () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indicesAPIClient: indicesAPIClientMock.create(), + }; + + testProps.indicesAPIClient.getFields.mockResolvedValue(['a', 'b', 'c']); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + }); + + test('does not query for available fields when mounted, and FLS is unavailable', async () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indicesAPIClient: indicesAPIClientMock.create(), + allowFieldLevelSecurity: false, + }; + + testProps.indicesAPIClient.getFields.mockResolvedValue(['a', 'b', 'c']); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(0); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(0); + expect(testProps.indicesAPIClient.getFields).not.toHaveBeenCalled(); + }); + + test('queries for available fields when the set of index patterns change', async () => { + const testProps = { + ...props, + indexPrivilege: { + ...props.indexPrivilege, + names: ['foo', 'bar-*'], + }, + indexPatterns: ['foo', 'bar-*', 'newPattern'], + indicesAPIClient: indicesAPIClientMock.create(), + }; + + testProps.indicesAPIClient.getFields.mockResolvedValue(['a', 'b', 'c']); + + const wrapper = mountWithIntl(); + await nextTick(); + expect(wrapper.find('div.indexPrivilegeForm__grantedFieldsRow')).toHaveLength(1); + expect(wrapper.find('div.indexPrivilegeForm__deniedFieldsRow')).toHaveLength(1); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(1); + + wrapper + .find(EuiComboBox) + .filterWhere((item) => item.props()['data-test-subj'] === 'indicesInput0') + .props().onChange!([{ label: 'newPattern', value: 'newPattern' }]); + + await nextTick(); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledTimes(2); + expect(testProps.indicesAPIClient.getFields).toHaveBeenCalledWith('newPattern'); }); test('it displays a warning when no fields are granted', () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index 697b5c1cac347b..811d5590b9a33e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -23,8 +23,10 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import type { RoleIndexPrivilege } from '../../../../../../common/model'; +import type { IndicesAPIClient } from '../../../indices_api_client'; import type { RoleValidator } from '../../validate_role'; const fromOption = (option: any) => option.label; @@ -35,7 +37,7 @@ interface Props { indexPrivilege: RoleIndexPrivilege; indexPatterns: string[]; availableIndexPrivileges: string[]; - availableFields: string[]; + indicesAPIClient: PublicMethodsOf; onChange: (indexPrivilege: RoleIndexPrivilege) => void; onDelete: () => void; isRoleReadOnly: boolean; @@ -50,9 +52,16 @@ interface State { grantedFields: string[]; exceptedFields: string[]; documentQuery?: string; + isFieldListLoading: boolean; + flsOptions: string[]; } export class IndexPrivilegeForm extends Component { + // This is distinct from the field within `this.state`. + // We want to make sure that only one request for fields is in-flight at a time, + // and relying on state for this is error prone. + private isFieldListLoading: boolean = false; + constructor(props: Props) { super(props); @@ -64,9 +73,17 @@ export class IndexPrivilegeForm extends Component { grantedFields: grant, exceptedFields: except, documentQuery: props.indexPrivilege.query, + isFieldListLoading: false, + flsOptions: [], }; } + public componentDidMount() { + if (this.state.fieldSecurityExpanded && this.props.allowFieldLevelSecurity) { + this.loadFLSOptions(this.props.indexPrivilege.names); + } + } + public render() { return ( @@ -149,11 +166,30 @@ export class IndexPrivilegeForm extends Component { ); }; + private loadFLSOptions = (indexNames: string[], force = false) => { + if (!force && (this.isFieldListLoading || indexNames.length === 0)) return; + + this.isFieldListLoading = true; + this.setState({ + isFieldListLoading: true, + }); + + this.props.indicesAPIClient + .getFields(indexNames.join(',')) + .then((fields) => { + this.isFieldListLoading = false; + this.setState({ flsOptions: fields, isFieldListLoading: false }); + }) + .catch(() => { + this.isFieldListLoading = false; + this.setState({ flsOptions: [], isFieldListLoading: false }); + }); + }; + private getFieldLevelControls = () => { const { allowFieldLevelSecurity, allowDocumentLevelSecurity, - availableFields, indexPrivilege, isRoleReadOnly, } = this.props; @@ -210,11 +246,13 @@ export class IndexPrivilegeForm extends Component { @@ -233,11 +271,13 @@ export class IndexPrivilegeForm extends Component { @@ -362,6 +402,11 @@ export class IndexPrivilegeForm extends Component { const hasSavedFieldSecurity = this.state.exceptedFields.length > 0 || this.state.grantedFields.length > 0; + // If turning on, then request available fields + if (willToggleOn) { + this.loadFLSOptions(this.props.indexPrivilege.names); + } + if (willToggleOn && !hasConfiguredFieldSecurity && hasSavedFieldSecurity) { this.props.onChange({ ...this.props.indexPrivilege, @@ -380,13 +425,22 @@ export class IndexPrivilegeForm extends Component { ...this.props.indexPrivilege, names: newIndexPatterns, }); + // If FLS controls are visible, then forcefully request a new set of options + if (this.state.fieldSecurityExpanded) { + this.loadFLSOptions(newIndexPatterns, true); + } }; private onIndexPatternsChange = (newPatterns: EuiComboBoxOptionOption[]) => { + const names = newPatterns.map(fromOption); this.props.onChange({ ...this.props.indexPrivilege, - names: newPatterns.map(fromOption), + names, }); + // If FLS controls are visible, then forcefully request a new set of options + if (this.state.fieldSecurityExpanded) { + this.loadFLSOptions(names, true); + } }; private onPrivilegeChange = (newPrivileges: EuiComboBoxOptionOption[]) => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx index 5b7d703865b9c3..c910ec5dda9a8e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.test.tsx @@ -56,6 +56,9 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () allowRoleDocumentLevelSecurity: true, } as any); + const indicesAPIClient = indicesAPIClientMock.create(); + indicesAPIClient.getFields.mockResolvedValue(['foo']); + const props = { role: { name: '', @@ -80,7 +83,7 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () editable: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], - indicesAPIClient: indicesAPIClientMock.create(), + indicesAPIClient, license, }; const wrapper = mountWithIntl(); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index 2f6bb73fc62fb0..d761992c275c9e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -41,18 +40,15 @@ export class IndexPrivileges extends Component { }; } - public componentDidMount() { - this.loadAvailableFields(this.props.role.elasticsearch.indices); - } - public render() { const { indices = [] } = this.props.role.elasticsearch; - const { indexPatterns, license, availableIndexPrivileges } = this.props; + const { indexPatterns, license, availableIndexPrivileges, indicesAPIClient } = this.props; const { allowRoleDocumentLevelSecurity, allowRoleFieldLevelSecurity } = license.getFeatures(); const props = { indexPatterns, + indicesAPIClient, // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that // doesn't permit FLS/DLS). @@ -69,7 +65,6 @@ export class IndexPrivileges extends Component { validator={this.props.validator} availableIndexPrivileges={availableIndexPrivileges} indexPrivilege={indexPrivilege} - availableFields={this.state.availableFields[indexPrivilege.names.join(',')]} onChange={this.onIndexPrivilegeChange(idx)} onDelete={this.onIndexPrivilegeDelete(idx)} /> @@ -116,8 +111,6 @@ export class IndexPrivileges extends Component { indices: newIndices, }, }); - - this.loadAvailableFields(newIndices); }; }; @@ -141,43 +134,4 @@ export class IndexPrivileges extends Component { public isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => { return indexPrivilege.names.length === 0; }; - - public loadAvailableFields(privileges: RoleIndexPrivilege[]) { - // readonly roles cannot be edited, and therefore do not need to fetch available fields. - if (isRoleReadOnly(this.props.role)) { - return; - } - - const patterns = privileges.map((index) => index.names.join(',')); - - const cachedPatterns = Object.keys(this.state.availableFields); - const patternsToFetch = _.difference(patterns, cachedPatterns); - - const fetchRequests = patternsToFetch.map(this.loadFieldsForPattern); - - Promise.all(fetchRequests).then((response) => { - this.setState({ - availableFields: { - ...this.state.availableFields, - ...response.reduce((acc, o) => ({ ...acc, ...o }), {}), - }, - }); - }); - } - - public loadFieldsForPattern = async (pattern: string) => { - if (!pattern) { - return { [pattern]: [] }; - } - - try { - return { - [pattern]: await this.props.indicesAPIClient.getFields(pattern), - }; - } catch (e) { - return { - [pattern]: [], - }; - } - }; } diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.test.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.test.ts new file mode 100644 index 00000000000000..feb0c73aa5bb2d --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { coreMock } from 'src/core/public/mocks'; + +import { IndicesAPIClient } from './indices_api_client'; + +describe('getFields', () => { + it('queries for available fields', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValue(['foo']); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo with special characters-&-*'); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith( + `/internal/security/fields/${encodeURIComponent('foo with special characters-&-*')}` + ); + expect(fields).toEqual(['foo']); + }); + + it('caches results for matching patterns', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValue(['foo']); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo'); + + const fields2 = await client.getFields('foo'); + + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith(`/internal/security/fields/${encodeURIComponent('foo')}`); + + expect(fields).toEqual(['foo']); + expect(fields2).toEqual(['foo']); + }); + + it('does not cache results for differing patterns', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValueOnce(['foo']); + http.get.mockResolvedValueOnce(['bar']); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo'); + + const fields2 = await client.getFields('bar'); + + expect(http.get).toHaveBeenCalledTimes(2); + expect(http.get).toHaveBeenNthCalledWith( + 1, + `/internal/security/fields/${encodeURIComponent('foo')}` + ); + expect(http.get).toHaveBeenNthCalledWith( + 2, + `/internal/security/fields/${encodeURIComponent('bar')}` + ); + + expect(fields).toEqual(['foo']); + expect(fields2).toEqual(['bar']); + }); + + it('does not cache empty results', async () => { + const { http } = coreMock.createSetup(); + http.get.mockResolvedValue([]); + + const client = new IndicesAPIClient(http); + + const fields = await client.getFields('foo'); + + const fields2 = await client.getFields('foo'); + + expect(http.get).toHaveBeenCalledTimes(2); + expect(http.get).toHaveBeenNthCalledWith( + 1, + `/internal/security/fields/${encodeURIComponent('foo')}` + ); + expect(http.get).toHaveBeenNthCalledWith( + 2, + `/internal/security/fields/${encodeURIComponent('foo')}` + ); + + expect(fields).toEqual([]); + expect(fields2).toEqual([]); + }); + + it('throws unexpected errors', async () => { + const { http } = coreMock.createSetup(); + http.get.mockRejectedValue(new Error('AHHHH')); + + const client = new IndicesAPIClient(http); + + await expect(() => client.getFields('foo')).rejects.toThrowErrorMatchingInlineSnapshot( + `"AHHHH"` + ); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.ts index ebec4d858d7ada..5d2ead1961fc64 100644 --- a/x-pack/plugins/security/public/management/roles/indices_api_client.ts +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.ts @@ -8,12 +8,20 @@ import type { HttpStart } from 'src/core/public'; export class IndicesAPIClient { + private readonly fieldCache = new Map(); + constructor(private readonly http: HttpStart) {} - async getFields(query: string) { - return ( - (await this.http.get(`/internal/security/fields/${encodeURIComponent(query)}`)) || - [] - ); + async getFields(pattern: string): Promise { + if (pattern && !this.fieldCache.has(pattern)) { + const fields = await this.http.get( + `/internal/security/fields/${encodeURIComponent(pattern)}` + ); + if (Array.isArray(fields) && fields.length > 0) { + this.fieldCache.set(pattern, fields); + } + } + + return this.fieldCache.get(pattern) ?? []; } } diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index 476aabb845c51e..4b10c2d5cf13ba 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -96,7 +96,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
`); @@ -124,7 +124,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
`); @@ -149,7 +149,7 @@ describe('rolesManagementApp', () => { expect(docTitle.reset).not.toHaveBeenCalled(); expect(container).toMatchInlineSnapshot(`
- Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"fieldCache":{}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
`); diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts index 63704682e36351..d0370a93b14c31 100644 --- a/x-pack/plugins/security/server/routes/indices/get_fields.ts +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -25,6 +25,7 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { fields: '*', allow_no_indices: false, include_defaults: true, + filter_path: '*.mappings.*.mapping.*.type', }); // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): @@ -62,7 +63,17 @@ export function defineGetFieldsRoutes({ router }: RouteDefinitionParams) { body: fields, }); } catch (error) { - return response.customError(wrapIntoCustomErrorResponse(error)); + const customResponse = wrapIntoCustomErrorResponse(error); + + // Elasticsearch returns a 404 response if the provided pattern does not match any indices. + // In this scenario, we want to instead treat this as an empty response. + if (customResponse.statusCode === 404) { + return response.ok({ + body: [], + }); + } + + return response.customError(customResponse); } } ); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 177967377f37f1..5af5a8adf95b7f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { JSON_LINES } from '../../screens/alerts_details'; +import { CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details'; import { expandFirstAlert, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; -import { openJsonView, scrollJsonViewToBottom } from '../../tasks/alerts_details'; +import { openJsonView, openTable, scrollJsonViewToBottom } from '../../tasks/alerts_details'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad } from '../../tasks/es_archiver'; @@ -30,12 +30,11 @@ describe('Alert details with unmapped fields', () => { waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); createCustomRuleActivated(unmappedRule); - }); - beforeEach(() => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); expandFirstAlert(); }); + it('Displays the unmapped field on the JSON view', () => { const expectedUnmappedField = { line: 2, text: ' "unmapped": "This is the unmapped field"' }; @@ -49,4 +48,21 @@ describe('Alert details with unmapped fields', () => { .should('have.text', expectedUnmappedField.text); }); }); + + it('Displays the unmapped field on the table', () => { + const expectedUnmmappedField = { + row: 55, + field: 'unmapped', + text: 'This is the unmapped field', + }; + + openTable(); + + cy.get(TABLE_ROWS) + .eq(expectedUnmmappedField.row) + .within(() => { + cy.get(CELL_TEXT).eq(0).should('have.text', expectedUnmmappedField.field); + cy.get(CELL_TEXT).eq(1).should('have.text', expectedUnmmappedField.text); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts index fdc003039afbc6..cb3785c1aba253 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { timeline } from '../../objects/timeline'; +import { timelineNonValidQuery } from '../../objects/timeline'; import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -19,14 +19,14 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; import { TIMELINES_URL } from '../../urls/navigation'; describe('Timeline notes tab', () => { - let timelineId: string | undefined; + let timelineId: string; before(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); - createTimeline(timeline) + createTimeline(timelineNonValidQuery) .then((response) => { if (response.body.data.persistTimeline.timeline.savedObjectId == null) { cy.log('"createTimeline" did not return expected response'); @@ -35,11 +35,8 @@ describe('Timeline notes tab', () => { waitForTimelinesPanelToBeLoaded(); }) .then(() => { - // TODO: It would be great to add response validation to avoid such things like - // the bang below and to more easily understand where failures are coming from - - // client vs server side - openTimelineById(timelineId!).then(() => { - addNotesToTimeline(timeline.notes); + openTimelineById(timelineId).then(() => { + addNotesToTimeline(timelineNonValidQuery.notes); }); }); }); @@ -49,7 +46,7 @@ describe('Timeline notes tab', () => { }); it('should contain notes', () => { - cy.get(NOTES_TEXT).should('have.text', timeline.notes); + cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes); }); it('should render mockdown', () => { diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index a3d653840dfd99..b5089c97756665 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -37,6 +37,16 @@ export const timeline: CompleteTimeline = { filter, }; +/** + * Timeline query that finds no valid data to cut down on test failures + * or other issues for when we want to test one specific thing and not also + * test the queries happening + */ +export const timelineNonValidQuery: CompleteTimeline = { + ...timeline, + query: 'query_to_intentionally_find_nothing: *', +}; + export const caseTimeline: Timeline = { title: 'SIEM test', description: 'description', diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index 417cf73de47f6d..c18949224d4c01 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -5,8 +5,14 @@ * 2.0. */ +export const CELL_TEXT = '.euiText'; + export const JSON_CONTENT = '[data-test-subj="jsonView"]'; export const JSON_LINES = '.ace_line'; export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]'; + +export const TABLE_TAB = '[data-test-subj="tableTab"]'; + +export const TABLE_ROWS = '.euiTableRow'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts index 45dcc6ea5de5e0..16c30afe117ef5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_details.ts @@ -5,12 +5,16 @@ * 2.0. */ -import { JSON_CONTENT, JSON_VIEW_TAB } from '../screens/alerts_details'; +import { JSON_CONTENT, JSON_VIEW_TAB, TABLE_TAB } from '../screens/alerts_details'; export const openJsonView = () => { cy.get(JSON_VIEW_TAB).click(); }; +export const openTable = () => { + cy.get(TABLE_TAB).click(); +}; + export const scrollJsonViewToBottom = () => { cy.get(JSON_CONTENT).click({ force: true }); cy.get(JSON_CONTENT).type('{pagedown}{pagedown}{pagedown}'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index a7842a7d98e831..e81cfa50a1a706 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -234,21 +234,14 @@ export const openTimelineTemplateFromSettings = (id: string) => { }; export const openTimelineById = (timelineId: string): Cypress.Chainable> => { - // Why are we checking for null if it is typed to 'string'? We don't currently validate the timeline response - // so technically we cannot guarantee that we will have the id. Changing the type to 'string | null' results in - // a lot of other changes being needed that would be best as part of a cleanup. Added a log, to give a dev a clue - // as to whether it's failing client or server side. if (timelineId == null) { + // Log out if for some reason this happens to be null just in case for our tests we experience + // value of null. Some tests return an "any" which is why this could happen. cy.log('"timelineId" is null or undefined'); } - - cy.root() - .pipe(($el) => { - $el.find(TIMELINE_TITLE_BY_ID(timelineId)).trigger('click'); - return $el.find(QUERY_TAB_BUTTON).find('.euiBadge'); - }) - .should('be.visible'); - return cy.root().find(TIMELINE_TITLE_BY_ID(timelineId)); + // We avoid use cypress.pipe() here and multiple clicks because each of these clicks + // can result in a new URL async operation occurring and then we get indeterminism as the URL loads multiple times. + return cy.get(TIMELINE_TITLE_BY_ID(timelineId)).should('be.visible').click({ force: true }); }; export const pinFirstEvent = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 91ebec72d38455..4e924402783c28 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -146,6 +146,7 @@ const EventDetailsComponent: React.FC = ({ const tableTab = useMemo( () => ({ id: EventsViewType.tableView, + 'data-test-subj': 'tableTab', name: i18n.TABLE, content: ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/agents_summary.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/agents_summary.tsx index 37b4f963bf920f..512b3d494faf60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/agents_summary.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/agents_summary.tsx @@ -35,7 +35,7 @@ export const AgentsSummary = memo((props) => { title: i18n.translate( 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle', { - defaultMessage: 'Endpoints', + defaultMessage: 'Agents', } ), health: '', @@ -45,30 +45,30 @@ export const AgentsSummary = memo((props) => { title: i18n.translate( 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.onlineTitle', { - defaultMessage: 'Online', + defaultMessage: 'Healthy', } ), health: 'success', }, { - key: 'offline', + key: 'error', title: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle', + 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle', { - defaultMessage: 'Offline', + defaultMessage: 'Unhealthy', } ), health: 'warning', }, { - key: 'error', + key: 'offline', title: i18n.translate( - 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle', + 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle', { - defaultMessage: 'Error', + defaultMessage: 'Offline', } ), - health: 'danger', + health: 'subdued', }, ]; }, []); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index ff5f4106110996..1407e5f64f156a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -161,7 +161,7 @@ describe('Policy Details', () => { const agentsSummary = policyView.find('EuiFlexGroup[data-test-subj="policyAgentsSummary"]'); expect(agentsSummary).toHaveLength(1); - expect(agentsSummary.text()).toBe('Endpoints5Online3Offline1Error1'); + expect(agentsSummary.text()).toBe('Agents5Healthy3Unhealthy1Offline1'); }); it('should display cancel button', async () => { await asyncActions; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts index 14a5b3e809a537..0331d1cec3a7fc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.test.ts @@ -19,7 +19,12 @@ describe('utils', () => { }); it('should parse complex query with term', () => { expect(parseQueryFilterToKQL('complex query')).toBe( - 'exception-list-agnostic.attributes.name:*complex* *query* OR exception-list-agnostic.attributes.description:*complex* *query* OR exception-list-agnostic.attributes.entries.value:*complex* *query* OR exception-list-agnostic.attributes.entries.entries.value:*complex* *query*' + 'exception-list-agnostic.attributes.name:*complex*query* OR exception-list-agnostic.attributes.description:*complex*query* OR exception-list-agnostic.attributes.entries.value:*complex*query* OR exception-list-agnostic.attributes.entries.entries.value:*complex*query*' + ); + }); + it('should parse complex query with special chars term', () => { + expect(parseQueryFilterToKQL('C:\\tmpes')).toBe( + 'exception-list-agnostic.attributes.name:*C\\:\\\\tmpes* OR exception-list-agnostic.attributes.description:*C\\:\\\\tmpes* OR exception-list-agnostic.attributes.entries.value:*C\\:\\\\tmpes* OR exception-list-agnostic.attributes.entries.entries.value:*C\\:\\\\tmpes*' ); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts index a2f19a60030561..aacea33087169e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/utils.ts @@ -10,7 +10,10 @@ export const parseQueryFilterToKQL = (filter: string): string => { const kuery = [`name`, `description`, `entries.value`, `entries.entries.value`] .map( (field) => - `exception-list-agnostic.attributes.${field}:*${filter.trim().replace(/\s/gm, '* *')}*` + `exception-list-agnostic.attributes.${field}:*${filter + .trim() + .replace(/([^a-zA-Z0-9\s/])/gm, '\\$&') + .replace(/\s/gm, '*')}*` ) .join(' OR '); diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index e607936be27419..9b24a70792030a 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -9,6 +9,15 @@ import { DEFAULT_SPACE_ID } from '../constants'; const spaceContextRegex = /^\/s\/([a-z0-9_\-]+)/; +/** + * Extracts the space id from the given path. + * + * @param requestBasePath The base path of the current request. + * @param serverBasePath The server's base path. + * @returns the space id. + * + * @private + */ export function getSpaceIdFromPath( requestBasePath?: string | null, serverBasePath?: string | null @@ -40,6 +49,15 @@ export function getSpaceIdFromPath( }; } +/** + * Given a server base path, space id, and requested resource, this will construct a space-aware path + * that includes a URL identifier with the space id. + * + * @param basePath the server's base path. + * @param spaceId the space id. + * @param requestedPath the requested path (e.g. `/app/dashboard`). + * @returns the space-aware version of the requested path, inclusive of the server's base path. + */ export function addSpaceIdToPath( basePath: string = '/', spaceId: string = '', diff --git a/x-pack/plugins/spaces/common/types.ts b/x-pack/plugins/spaces/common/types.ts index 46455c1f601d4c..866d29bf64d5b6 100644 --- a/x-pack/plugins/spaces/common/types.ts +++ b/x-pack/plugins/spaces/common/types.ts @@ -7,17 +7,46 @@ import type { Space } from 'src/plugins/spaces_oss/common'; +/** + * Controls how spaces are retrieved. + */ export interface GetAllSpacesOptions { + /** + * An optional purpose describing how the set of spaces will be used. + * The default purpose (`any`) will retrieve all spaces the user is authorized to see, + * whereas a more specific purpose will retrieve all spaces the user is authorized to perform a specific action within. + * + * @see GetAllSpacesPurpose + */ purpose?: GetAllSpacesPurpose; + + /** + * Set to true to return a set of flags indicating which purposes the user is authorized for. + * + * @see GetAllSpacesPurpose + */ includeAuthorizedPurposes?: boolean; } +/** + * The set of purposes to retrieve spaces: + * - `any`: retrieves all spaces the user is authorized to see. + * - `copySavedObjectsIntoSpace`: retrieves all spaces the user is authorized to copy saved objects into. + * - `findSavedObjects`: retrieves all spaces the user is authorized to search within. + * - `shareSavedObjectsIntoSpace`: retrieves all spaces the user is authorized to share saved objects into. + */ export type GetAllSpacesPurpose = | 'any' | 'copySavedObjectsIntoSpace' | 'findSavedObjects' | 'shareSavedObjectsIntoSpace'; +/** + * Response format when querying for spaces. + */ export interface GetSpaceResult extends Space { + /** + * A set of flags indicating which purposes the user is authorized for. + */ authorizedPurposes?: Record; } diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 062a534c91c840..88926715518966 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -36,7 +36,14 @@ export interface PluginsStart { management?: ManagementStart; } +/** + * Setup contract for the Spaces plugin. + */ export type SpacesPluginSetup = ReturnType; + +/** + * Start contract for the Spaces plugin. + */ export type SpacesPluginStart = ReturnType; export class SpacesPlugin implements Plugin { diff --git a/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts b/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts index e37d7eed322e1f..682a61c6f23a53 100644 --- a/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts +++ b/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts @@ -19,7 +19,7 @@ const FALLBACK_CODE_POINT = 97; * If a color is present on the Space itself, then that is used. * Otherwise, a color is provided from EUI's Visualization Colors based on the space name. * - * @param {Space} space + * @param {Space} space the space. */ export function getSpaceColor(space: Partial = {}) { const { color, name = '' } = space; @@ -38,7 +38,7 @@ export function getSpaceColor(space: Partial = {}) { * If initials are present on the Space itself, then that is used. * Otherwise, the initials are calculated based off the words in the space name, with a max length of 2 characters. * - * @param {Space} space + * @param {Space} space the space. */ export function getSpaceInitials(space: Partial = {}) { const { initials, name = '' } = space; @@ -59,7 +59,7 @@ export function getSpaceInitials(space: Partial = {}) { /** * Determines the avatar image for the provided space. * - * @param {Space} space + * @param {Space} space the space. */ export function getSpaceImageUrl(space: Partial = {}) { const { imageUrl } = space; diff --git a/x-pack/plugins/spaces/server/config.test.ts b/x-pack/plugins/spaces/server/config.test.ts index 1ce1be0698b1c0..1e60c1b6353209 100644 --- a/x-pack/plugins/spaces/server/config.test.ts +++ b/x-pack/plugins/spaces/server/config.test.ts @@ -37,7 +37,7 @@ describe('spaces config', () => { expect(messages).toMatchInlineSnapshot(` Array [ - "Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)", + "Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)", ] `); expect(migrated).toEqual(originalConfig); diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index ed541fda6c292b..7f8b1a29befb3e 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -27,7 +27,7 @@ export function createConfig$(context: PluginInitializerContext) { const disabledDeprecation: ConfigDeprecation = (config, fromPath, addDeprecation) => { if (config.xpack?.spaces?.enabled === false) { addDeprecation({ - message: `Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)`, + message: `Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)`, }); } return config; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 4218d8518720cd..c779e92a3ae9b7 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -21,7 +21,7 @@ export { addSpaceIdToPath } from '../common'; export { SpacesPluginSetup, SpacesPluginStart } from './plugin'; export { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; -export { ISpacesClient } from './spaces_client'; +export { ISpacesClient, SpacesClientRepositoryFactory, SpacesClientWrapper } from './spaces_client'; export { GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult } from '../common'; diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index 94236b6e7f589f..c5732fc4045bb5 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -14,7 +14,7 @@ import { licensingMock } from '../../licensing/server/mocks'; import type { PluginsStart } from './plugin'; import { SpacesPlugin } from './plugin'; -describe('Spaces Plugin', () => { +describe('Spaces plugin', () => { describe('#setup', () => { it('can setup with all optional plugins disabled, exposing the expected contract', () => { const initializerContext = coreMock.createPluginInitializerContext({}); diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index aaa19ee74cb744..a7400fb71c1db1 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -51,15 +51,41 @@ export interface PluginsStart { features: FeaturesPluginStart; } +/** + * Setup contract for the Spaces plugin. + */ export interface SpacesPluginSetup { + /** + * Service for interacting with spaces. + * + * @deprecated Please use the `spacesService` available on this plugin's start contract. + * @removeBy 7.16 + */ spacesService: SpacesServiceSetup; + + /** + * Registries exposed for the security plugin to transparently provide authorization and audit logging. + * @private + */ spacesClient: { + /** + * Sets the client repository factory. + * @private + */ setClientRepositoryFactory: (factory: SpacesClientRepositoryFactory) => void; + /** + * Registers a client wrapper. + * @private + */ registerClientWrapper: (wrapper: SpacesClientWrapper) => void; }; } +/** + * Start contract for the Spaces plugin. + */ export interface SpacesPluginStart { + /** Service for interacting with spaces. */ spacesService: SpacesServiceStart; } diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts index c4f5e9edaf4943..02aa4d0b976c0c 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts @@ -8,7 +8,6 @@ import Boom from '@hapi/boom'; import { omit } from 'lodash'; -import type { PublicMethodsOf } from '@kbn/utility-types'; import type { ISavedObjectsRepository, SavedObject } from 'src/core/server'; import type { Space } from 'src/plugins/spaces_oss/common'; @@ -24,9 +23,46 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetAllSpacesPurpose[] = [ ]; const DEFAULT_PURPOSE = 'any'; -export type ISpacesClient = PublicMethodsOf; +/** + * Client interface for interacting with spaces. + */ +export interface ISpacesClient { + /** + * Retrieve all available spaces. + * @param options controls which spaces are retrieved. + */ + getAll(options?: GetAllSpacesOptions): Promise; + + /** + * Retrieve a space by its id. + * @param id the space id. + */ + get(id: string): Promise; + + /** + * Creates a space. + * @param space the space to create. + */ + create(space: Space): Promise; + + /** + * Updates a space. + * @param id the id of the space to update. + * @param space the updated space. + */ + update(id: string, space: Space): Promise; + + /** + * Deletes a space, and all saved objects belonging to that space. + * @param id the id of the space to delete. + */ + delete(id: string): Promise; +} -export class SpacesClient { +/** + * Client for interacting with spaces. + */ +export class SpacesClient implements ISpacesClient { constructor( private readonly debugLogger: (message: string) => void, private readonly config: ConfigType, diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts index 4db27bac845f1b..6580a2d57f040b 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client_service.ts @@ -18,11 +18,19 @@ import type { ConfigType } from '../config'; import type { ISpacesClient } from './spaces_client'; import { SpacesClient } from './spaces_client'; +/** + * For consumption by the security plugin only. + * @private + */ export type SpacesClientWrapper = ( request: KibanaRequest, baseClient: ISpacesClient ) => ISpacesClient; +/** + * For consumption by the security plugin only. + * @private + */ export type SpacesClientRepositoryFactory = ( request: KibanaRequest, savedObjectsStart: SavedObjectsServiceStart diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index c76646fa8a2b47..e951ed38072d71 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -11,67 +11,77 @@ import type { Space } from 'src/plugins/spaces_oss/common'; import { getSpaceIdFromPath } from '../../common'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { namespaceToSpaceId, spaceIdToNamespace } from '../lib/utils/namespace'; -import type { SpacesClientServiceStart } from '../spaces_client'; +import type { ISpacesClient, SpacesClientServiceStart } from '../spaces_client'; +/** + * The Spaces service setup contract. + */ export interface SpacesServiceSetup { /** * Retrieves the space id associated with the provided request. - * @param request + * @param request the request. * * @deprecated Use `getSpaceId` from the `SpacesServiceStart` contract instead. + * @removeBy 7.16 */ getSpaceId(request: KibanaRequest): string; /** * Converts the provided space id into the corresponding Saved Objects `namespace` id. - * @param spaceId + * @param spaceId the space id to convert. * * @deprecated use `spaceIdToNamespace` from the `SpacesServiceStart` contract instead. + * @removeBy 7.16 */ spaceIdToNamespace(spaceId: string): string | undefined; /** * Converts the provided namespace into the corresponding space id. - * @param namespace + * @param namespace the namespace to convert. * * @deprecated use `namespaceToSpaceId` from the `SpacesServiceStart` contract instead. + * @removeBy 7.16 */ namespaceToSpaceId(namespace: string | undefined): string; } +/** + * The Spaces service start contract. + */ export interface SpacesServiceStart { /** * Creates a scoped instance of the SpacesClient. + * @param request the request. */ - createSpacesClient: SpacesClientServiceStart['createSpacesClient']; + createSpacesClient: (request: KibanaRequest) => ISpacesClient; /** * Retrieves the space id associated with the provided request. - * @param request + * @param request the request. */ getSpaceId(request: KibanaRequest): string; /** * Indicates if the provided request is executing within the context of the `default` space. - * @param request + * @param request the request. */ isInDefaultSpace(request: KibanaRequest): boolean; /** * Retrieves the Space associated with the provided request. - * @param request + * @param request the request. */ getActiveSpace(request: KibanaRequest): Promise; /** * Converts the provided space id into the corresponding Saved Objects `namespace` id. - * @param spaceId + * @param spaceId the space id to convert. */ spaceIdToNamespace(spaceId: string): string | undefined; /** * Converts the provided namespace into the corresponding space id. - * @param namespace + * @param namespace the namespace to convert. */ namespaceToSpaceId(namespace: string | undefined): string; } @@ -85,6 +95,9 @@ interface SpacesServiceStartDeps { spacesClientService: SpacesClientServiceStart; } +/** + * Service for interacting with spaces. + */ export class SpacesService { public setup({ basePath }: SpacesServiceSetupDeps): SpacesServiceSetup { return { @@ -96,7 +109,7 @@ export class SpacesService { }; } - public start({ basePath, spacesClientService }: SpacesServiceStartDeps) { + public start({ basePath, spacesClientService }: SpacesServiceStartDeps): SpacesServiceStart { return { getSpaceId: (request: KibanaRequest) => { return this.getSpaceId(request, basePath); diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index ef001edd429fd6..a43171fc3b4640 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -338,13 +338,13 @@ export function getSpacesUsageCollector( available: { type: 'boolean', _meta: { - description: 'Indicates if the spaces feature is available in this installation.', + description: 'Indicates if the Spaces feature is available in this installation.', }, }, enabled: { type: 'boolean', _meta: { - description: 'Indicates if the spaces feature is enabled in this installation.', + description: 'Indicates if the Spaces feature is enabled in this installation.', }, }, count: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 688c256d308809..9c42ce424b448e 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -5218,13 +5218,13 @@ "available": { "type": "boolean", "_meta": { - "description": "Indicates if the spaces feature is available in this installation." + "description": "Indicates if the Spaces feature is available in this installation." } }, "enabled": { "type": "boolean", "_meta": { - "description": "Indicates if the spaces feature is enabled in this installation." + "description": "Indicates if the Spaces feature is enabled in this installation." } }, "count": { diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 934e0b07f9d38d..6cdb01a992034c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -14,7 +14,6 @@ import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { OpenModal, CommonlyUsedRange } from './types'; export const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE'; -const SEARCH_EMBEDDABLE_TYPE = 'search'; export interface TimeRangeInput extends EmbeddableInput { timeRange: TimeRange; @@ -79,15 +78,7 @@ export class CustomTimeRangeAction implements Action { const isMarkdown = isVisualizeEmbeddable(embeddable) && (embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown'; - return Boolean( - embeddable && - hasTimeRange(embeddable) && - // Saved searches don't listen to the time range from the container that is passed down to them so it - // won't work without a fix. For now, just leave them out. - embeddable.type !== SEARCH_EMBEDDABLE_TYPE && - !isInputControl && - !isMarkdown - ); + return Boolean(embeddable && hasTimeRange(embeddable) && !isInputControl && !isMarkdown); } public async execute({ embeddable }: TimeRangeActionContext) { diff --git a/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts b/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts index 33d7177cf15f9b..fd7cd2c90e9522 100644 --- a/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts +++ b/x-pack/plugins/upgrade_assistant/tests_client_integration/overview.test.ts @@ -49,7 +49,7 @@ describe('Overview page', () => { domainId: 'xpack.spaces', level: 'critical', message: - 'Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)', + 'Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)', }, ]; diff --git a/x-pack/test/api_integration/apis/file_upload/index.ts b/x-pack/test/api_integration/apis/file_upload/index.ts index cf0159997fa112..30fd1ae819598d 100644 --- a/x-pack/test/api_integration/apis/file_upload/index.ts +++ b/x-pack/test/api_integration/apis/file_upload/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('File upload', function () { loadTestFile(require.resolve('./has_import_permission')); + loadTestFile(require.resolve('./index_exists')); }); } diff --git a/x-pack/test/api_integration/apis/file_upload/index_exists.ts b/x-pack/test/api_integration/apis/file_upload/index_exists.ts new file mode 100644 index 00000000000000..4e014105ab7a87 --- /dev/null +++ b/x-pack/test/api_integration/apis/file_upload/index_exists.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('POST /internal/file_upload/index_exists', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('logstash_functional'); + }); + + it('should return true when index exists', async () => { + const resp = await supertest + .post(`/internal/file_upload/index_exists`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'logstash-2015.09.22', + }) + .expect(200); + + expect(resp.body.exists).to.be(true); + }); + + it('should return false when index does not exists', async () => { + const resp = await supertest + .post(`/internal/file_upload/index_exists`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'myNewIndex', + }) + .expect(200); + + expect(resp.body.exists).to.be(false); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/security/index_fields.ts b/x-pack/test/api_integration/apis/security/index_fields.ts index 6918ec3415dac4..442740c7666df9 100644 --- a/x-pack/test/api_integration/apis/security/index_fields.ts +++ b/x-pack/test/api_integration/apis/security/index_fields.ts @@ -71,6 +71,14 @@ export default function ({ getService }: FtrProviderContext) { expect(actualFields).to.eql(expectedFields); }); + + it('should return an empty result for indices that do not exist', async () => { + await supertest + .get('/internal/security/fields/this-index-name-definitely-does-not-exist-*') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200, []); + }); }); }); } diff --git a/x-pack/test/case_api_integration/basic/tests/cases/alerts/get_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/alerts/get_cases.ts new file mode 100644 index 00000000000000..140fb80949a24b --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/alerts/get_cases.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentAlertReq } from '../../../../common/lib/mock'; +import { deleteAllCaseItems } from '../../../../common/lib/utils'; +import { CaseResponse } from '../../../../../../plugins/cases/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_cases using alertID', () => { + const createCase = async () => { + const { body } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + return body; + }; + + const createComment = async (caseID: string) => { + await supertest + .post(`${CASES_URL}/${caseID}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); + }; + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return all cases with the same alert ID attached to them', async () => { + const [case1, case2, case3] = await Promise.all([createCase(), createCase(), createCase()]); + + await Promise.all([ + createComment(case1.id), + createComment(case2.id), + createComment(case3.id), + ]); + + const { body: caseIDsWithAlert } = await supertest + .get(`${CASES_URL}/alerts/test-id`) + .expect(200); + + expect(caseIDsWithAlert.length).to.eql(3); + expect(caseIDsWithAlert).to.contain(case1.id); + expect(caseIDsWithAlert).to.contain(case2.id); + expect(caseIDsWithAlert).to.contain(case3.id); + }); + + it('should return all cases with the same alert ID when more than 100 cases', async () => { + // if there are more than 100 responses, the implementation sets the aggregation size to the + // specific value + const numCases = 102; + const createCasePromises: Array> = []; + for (let i = 0; i < numCases; i++) { + createCasePromises.push(createCase()); + } + + const cases = await Promise.all(createCasePromises); + + const commentPromises: Array> = []; + for (const caseInfo of cases) { + commentPromises.push(createComment(caseInfo.id)); + } + + await Promise.all(commentPromises); + + const { body: caseIDsWithAlert } = await supertest + .get(`${CASES_URL}/alerts/test-id`) + .expect(200); + + expect(caseIDsWithAlert.length).to.eql(numCases); + + for (const caseInfo of cases) { + expect(caseIDsWithAlert).to.contain(caseInfo.id); + } + }); + + it('should return no cases when the alert ID is not found', async () => { + const [case1, case2, case3] = await Promise.all([createCase(), createCase(), createCase()]); + + await Promise.all([ + createComment(case1.id), + createComment(case2.id), + createComment(case3.id), + ]); + + const { body: caseIDsWithAlert } = await supertest + .get(`${CASES_URL}/alerts/test-id100`) + .expect(200); + + expect(caseIDsWithAlert.length).to.eql(0); + }); + + it('should return a 302 when passing an empty alertID', async () => { + // kibana returns a 302 instead of a 400 when a url param is missing + await supertest.get(`${CASES_URL}/alerts/`).expect(302); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts index 837e6503084a70..24e6b121388955 100644 --- a/x-pack/test/case_api_integration/basic/tests/index.ts +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -13,6 +13,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { // Fastest ciGroup for the moment. this.tags('ciGroup5'); + loadTestFile(require.resolve('./cases/alerts/get_cases')); loadTestFile(require.resolve('./cases/comments/delete_comment')); loadTestFile(require.resolve('./cases/comments/find_comments')); loadTestFile(require.resolve('./cases/comments/get_comment')); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 192ecb0d4a1f93..82f53929ada313 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -16,6 +16,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./async_scripted_fields')); loadTestFile(require.resolve('./reporting')); loadTestFile(require.resolve('./error_handling')); + loadTestFile(require.resolve('./saved_searches')); loadTestFile(require.resolve('./visualize_field')); loadTestFile(require.resolve('./value_suggestions')); }); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts new file mode 100644 index 00000000000000..f811e705e1967c --- /dev/null +++ b/x-pack/test/functional/apps/discover/saved_searches.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; 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 ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'timePicker', 'dashboard']); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dataGrid = getService('dataGrid'); + const panelActions = getService('dashboardPanelActions'); + const panelActionsTimeRange = getService('dashboardPanelTimeRange'); + + describe('Discover Saved Searches', () => { + before('initialize tests', async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + await kibanaServer.uiSettings.unset('doc_table:legacy'); + }); + + describe('Customize time range', () => { + it('should be possible to customize time range for saved searches on dashboards', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.addSavedSearch('Ecommerce Data'); + expect(await dataGrid.getDocCount()).to.be(500); + await panelActions.openContextMenuMorePanel(); + await panelActionsTimeRange.clickTimeRangeActionInContextMenu(); + await panelActionsTimeRange.clickToggleQuickMenuButton(); + await panelActionsTimeRange.clickCommonlyUsedTimeRange('Last_90 days'); + await panelActionsTimeRange.clickModalPrimaryButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await dataGrid.hasNoResults()).to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index e7b5df70c99a0b..4de95a5d82054b 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -16,5 +16,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./classification_creation')); loadTestFile(require.resolve('./cloning')); loadTestFile(require.resolve('./feature_importance')); + loadTestFile(require.resolve('./trained_models')); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/trained_models.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/trained_models.ts new file mode 100644 index 00000000000000..43130651cb121d --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/trained_models.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + describe('trained models', function () { + before(async () => { + await ml.trainedModels.createdTestTrainedModels('classification', 15); + await ml.trainedModels.createdTestTrainedModels('regression', 15); + await ml.securityUI.loginAsMlPowerUser(); + await ml.navigation.navigateToTrainedModels(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('renders trained models list', async () => { + await ml.trainedModels.assertRowsNumberPerPage(10); + // +1 because of the built-in model + await ml.trainedModels.assertStats(31); + }); + }); +} diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 7d09deff6f6b77..c0e3dedd8e191a 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -23,6 +23,7 @@ import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE, } from '../../../../plugins/ml/common/constants/index_patterns'; import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { PutTrainedModelConfig } from '../../../../plugins/ml/common/types/trained_models'; export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('es'); @@ -935,5 +936,17 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } } }, + + async createTrainedModel(modelId: string, body: PutTrainedModelConfig) { + log.debug(`Creating trained model with id "${modelId}"`); + const model = await esSupertest + .put(`/_ml/trained_models/${modelId}`) + .send(body) + .expect(200) + .then((res: any) => res.body); + + log.debug('> Trained model crated'); + return model; + }, }; } diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index b7ed95ef76ece3..f42f54116c926e 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -245,5 +245,28 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte ); }); }, + + async assertRowsNumberPerPage(testSubj: string, rowsNumber: 10 | 25 | 100) { + const textContent = await testSubjects.getVisibleText( + `${testSubj} > tablePaginationPopoverButton` + ); + expect(textContent).to.be(`Rows per page: ${rowsNumber}`); + }, + + async ensurePagePopupOpen(testSubj: string) { + await retry.tryForTime(5000, async () => { + const isOpen = await testSubjects.exists('tablePagination-10-rows'); + if (!isOpen) { + await testSubjects.click(`${testSubj} > tablePaginationPopoverButton`); + await testSubjects.existOrFail('tablePagination-10-rows'); + } + }); + }, + + async setRowsNumberPerPage(testSubj: string, rowsNumber: 10 | 25 | 100) { + await this.ensurePagePopupOpen(testSubj); + await testSubjects.click(`tablePagination-${rowsNumber}-rows`); + await this.assertRowsNumberPerPage(testSubj, rowsNumber); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 05d369d890289c..6a2e1158e70a37 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -48,6 +48,7 @@ import { MachineLearningAlertingProvider } from './alerting'; import { SwimLaneProvider } from './swim_lane'; import { MachineLearningDashboardJobSelectionTableProvider } from './dashboard_job_selection_table'; import { MachineLearningDashboardEmbeddablesProvider } from './dashboard_embeddables'; +import { TrainedModelsProvider } from './trained_models'; export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); @@ -108,6 +109,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const testResources = MachineLearningTestResourcesProvider(context); const alerting = MachineLearningAlertingProvider(context, commonUI); const swimLane = SwimLaneProvider(context); + const trainedModels = TrainedModelsProvider(context, api, commonUI); return { anomaliesTable, @@ -151,5 +153,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { swimLane, testExecution, testResources, + trainedModels, }; } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 075c788a863363..9bebc25f2de4cd 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -115,6 +115,13 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~dataFrameAnalytics', 'mlPageDataFrameAnalytics'); }, + async navigateToTrainedModels() { + await this.navigateToMl(); + await this.navigateToDataFrameAnalytics(); + await testSubjects.click('mlTrainedModelsTab'); + await testSubjects.existOrFail('mlModelsTableContainer'); + }, + async navigateToDataVisualizer() { await this.navigateToArea('~mlMainTab & ~dataVisualizer', 'mlPageDataVisualizerSelector'); }, diff --git a/x-pack/test/functional/services/ml/resources/trained_model_definitions/minimum_valid_config_classification.json.gz.b64 b/x-pack/test/functional/services/ml/resources/trained_model_definitions/minimum_valid_config_classification.json.gz.b64 new file mode 100644 index 00000000000000..c3e3a50f52d913 --- /dev/null +++ b/x-pack/test/functional/services/ml/resources/trained_model_definitions/minimum_valid_config_classification.json.gz.b64 @@ -0,0 +1 @@ +H4sICOE6Ol8AA2NsZi5qc29uAD2MQQqAIBBF955CXHeCrhIhg44xYBo6LUK8e1rW4i/ef59fhJSKE1BAq/do0atZllY+NeJPjR0Cnwl1gB1zE8sQXcWoBq3Tt2dIG7Lm6+g3ynjImRwZYIrhnVfRU28zTg0thgAAAA== \ No newline at end of file diff --git a/x-pack/test/functional/services/ml/resources/trained_model_definitions/minimum_valid_config_regression.json.gz.b64 b/x-pack/test/functional/services/ml/resources/trained_model_definitions/minimum_valid_config_regression.json.gz.b64 new file mode 100644 index 00000000000000..05c1aec7f149aa --- /dev/null +++ b/x-pack/test/functional/services/ml/resources/trained_model_definitions/minimum_valid_config_regression.json.gz.b64 @@ -0,0 +1 @@ +H4sICOc8Ol8AA3JnLmpzb24APYxBCoAgFET3nkJcd4KuEiGCkwip8f0tQrx7WtZiFm/eMEVIqZiMj7A6JItdzbK08qmBnxpvMHwSdDQBuYlliK5SUoPW6duzIQfWfB39RhEcIWef4jutoqfeWtCVIIIAAAA= \ No newline at end of file diff --git a/x-pack/test/functional/services/ml/trained_models.ts b/x-pack/test/functional/services/ml/trained_models.ts new file mode 100644 index 00000000000000..ae799efbbd30cb --- /dev/null +++ b/x-pack/test/functional/services/ml/trained_models.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fs from 'fs'; +import path from 'path'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlApi } from './api'; +import { PutTrainedModelConfig } from '../../../../plugins/ml/common/types/trained_models'; +import { MlCommonUI } from './common_ui'; + +type ModelType = 'regression' | 'classification'; + +export function TrainedModelsProvider( + { getService }: FtrProviderContext, + mlApi: MlApi, + mlCommonUI: MlCommonUI +) { + const testSubjects = getService('testSubjects'); + + return { + async createdTestTrainedModels(modelType: ModelType, count: number = 10) { + const compressedDefinition = this.getCompressedModelDefinition(modelType); + + const models = new Array(count).fill(null).map((v, i) => { + return { + model_id: `dfa_${modelType}_model_n_${i}`, + body: { + compressed_definition: compressedDefinition, + inference_config: { + [modelType]: {}, + }, + input: { + field_names: ['common_field'], + }, + } as PutTrainedModelConfig, + }; + }); + + for (const model of models) { + await mlApi.createTrainedModel(model.model_id, model.body); + } + }, + + getCompressedModelDefinition(modelType: ModelType) { + return fs.readFileSync( + path.resolve( + __dirname, + 'resources', + 'trained_model_definitions', + `minimum_valid_config_${modelType}.json.gz.b64` + ), + 'utf-8' + ); + }, + + async assertStats(expectedTotalCount: number) { + const actualStats = await testSubjects.getVisibleText('mlInferenceModelsStatsBar'); + expect(actualStats).to.eql(`Total trained models: ${expectedTotalCount}`); + }, + + async assertRowsNumberPerPage(rowsNumber: 10 | 25 | 100) { + await mlCommonUI.assertRowsNumberPerPage('mlModelsTableContainer', rowsNumber); + }, + }; +} diff --git a/yarn.lock b/yarn.lock index 076c3bd2871bdd..699f10936dc75b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2619,7 +2619,7 @@ version "0.0.0" uid "" -"@kbn/crypto@link:packages/kbn-crypto": +"@kbn/crypto@link:bazel-bin/packages/kbn-crypto/npm_module": version "0.0.0" uid "" @@ -28221,6 +28221,11 @@ vega-hierarchy@~4.0.9: vega-dataflow "^5.7.3" vega-util "^1.15.2" +vega-interpreter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vega-interpreter/-/vega-interpreter-1.0.4.tgz#291ebf85bc2d1c3550a3da22ff75b3ba0d326a39" + integrity sha512-6tpYIa/pJz0cZo5fSxDSkZkAA51pID2LjOtQkOQvbzn+sJiCaWKPFhur8MBqbcmYZ9bnap1OYNwlrvpd2qBLvg== + vega-label@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.0.0.tgz#c3bea3a608a62217ca554ecc0f7fe0395d81bd1b"