diff --git a/api_docs/cases.json b/api_docs/cases.json index 55ec344cb0bcb..bc92995dff6e9 100644 --- a/api_docs/cases.json +++ b/api_docs/cases.json @@ -1035,7 +1035,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 53 + "lineNumber": 49 }, "deprecated": false, "children": [ @@ -1051,7 +1051,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 53 + "lineNumber": 49 }, "deprecated": false, "isRequired": true @@ -1074,7 +1074,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 59 + "lineNumber": 55 }, "deprecated": false, "children": [ @@ -1091,7 +1091,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 60 + "lineNumber": 56 }, "deprecated": false, "isRequired": true @@ -1108,7 +1108,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 61 + "lineNumber": 57 }, "deprecated": false, "isRequired": true @@ -1129,7 +1129,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 75 + "lineNumber": 71 }, "deprecated": false, "children": [ @@ -1145,7 +1145,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 76 + "lineNumber": 72 }, "deprecated": false, "isRequired": true @@ -1170,7 +1170,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 24 + "lineNumber": 20 }, "deprecated": true, "references": [], @@ -1187,7 +1187,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 24 + "lineNumber": 20 }, "deprecated": false, "isRequired": true @@ -1634,62 +1634,6 @@ "returnComment": [], "initialIsOpen": false }, - { - "parentPluginId": "cases", - "id": "def-common.OmitProp", - "type": "Function", - "tags": [], - "label": "OmitProp", - "description": [], - "signature": [ - "(o: O, k: K) => Pick>" - ], - "source": { - "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 17 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "cases", - "id": "def-common.OmitProp.$1", - "type": "Uncategorized", - "tags": [], - "label": "o", - "description": [], - "signature": [ - "O" - ], - "source": { - "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 17 - }, - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "cases", - "id": "def-common.OmitProp.$2", - "type": "Uncategorized", - "tags": [], - "label": "k", - "description": [], - "signature": [ - "K" - ], - "source": { - "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 17 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "cases", "id": "def-common.throwErrors", @@ -1704,7 +1648,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 55 + "lineNumber": 51 }, "deprecated": false, "children": [ @@ -1720,7 +1664,7 @@ ], "source": { "path": "x-pack/plugins/cases/common/api/runtime_types.ts", - "lineNumber": 55 + "lineNumber": 51 }, "deprecated": false, "isRequired": true @@ -5278,7 +5222,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; })[]" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; })[]" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5335,7 +5279,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5392,7 +5336,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5506,7 +5450,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -5563,7 +5507,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; }" + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; } & { mappings: { action_type: \"append\" | \"overwrite\" | \"nothing\"; source: \"description\" | \"title\" | \"comments\"; target: string; }[]; owner: string; } & { id: string; version: string; error: string | null; owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", @@ -7187,7 +7131,7 @@ "section": "def-common.ConnectorTypes", "text": "ConnectorTypes" }, - ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }, \"updated_at\" | \"owner\" | \"created_at\" | \"created_by\" | \"updated_by\" | \"closure_type\"> & { connector: ", + ".none; fields: null; }); closure_type: \"close-by-user\" | \"close-by-pushing\"; } & { owner: string; } & { created_at: string; created_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; }; updated_at: string | null; updated_by: { email: string | null | undefined; full_name: string | null | undefined; username: string | null | undefined; } | null; }, \"updated_at\" | \"owner\" | \"created_at\" | \"created_by\" | \"updated_by\" | \"closure_type\"> & { connector: ", { "pluginId": "cases", "scope": "common", @@ -7359,6 +7303,40 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "cases", + "id": "def-common.MAX_CONCURRENT_SEARCHES", + "type": "number", + "tags": [], + "label": "MAX_CONCURRENT_SEARCHES", + "description": [], + "signature": [ + "10" + ], + "source": { + "path": "x-pack/plugins/cases/common/constants.ts", + "lineNumber": 97 + }, + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-common.MAX_DOCS_PER_PAGE", + "type": "number", + "tags": [], + "label": "MAX_DOCS_PER_PAGE", + "description": [], + "signature": [ + "10000" + ], + "source": { + "path": "x-pack/plugins/cases/common/constants.ts", + "lineNumber": 96 + }, + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "cases", "id": "def-common.MAX_GENERATED_ALERTS_PER_SUB_CASE", @@ -9365,6 +9343,8 @@ "<[", "IntersectionC", "<[", + "IntersectionC", + "<[", "TypeC", "<{ connector: ", "IntersectionC", @@ -9572,9 +9552,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>, ", + "; }>]>, ", "TypeC", "<{ created_at: ", "StringC", @@ -9701,6 +9683,8 @@ "label": "CaseConfigureAttributesRt", "description": [], "signature": [ + "IntersectionC", + "<[", "IntersectionC", "<[", "TypeC", @@ -9910,9 +9894,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>, ", + "; }>]>, ", "TypeC", "<{ created_at: ", "StringC", @@ -10019,6 +10005,8 @@ "<[", "IntersectionC", "<[", + "IntersectionC", + "<[", "TypeC", "<{ connector: ", "IntersectionC", @@ -10226,9 +10214,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>, ", + "; }>]>, ", "TypeC", "<{ created_at: ", "StringC", @@ -12728,7 +12718,7 @@ "IntersectionC", "<[", "PartialC", - ", ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", - "StringC", - "; }, \"connector\" | \"closure_type\">>, ", + "<\"close-by-pushing\">]>; }>, ", "TypeC", "<{ version: ", "StringC", @@ -12957,6 +12945,8 @@ "label": "CasesConfigureRequestRt", "description": [], "signature": [ + "IntersectionC", + "<[", "TypeC", "<{ connector: ", "IntersectionC", @@ -13164,9 +13154,11 @@ "LiteralC", "<\"close-by-user\">, ", "LiteralC", - "<\"close-by-pushing\">]>; owner: ", + "<\"close-by-pushing\">]>; }>, ", + "TypeC", + "<{ owner: ", "StringC", - "; }>" + "; }>]>" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/configure.ts", diff --git a/api_docs/licensing.json b/api_docs/licensing.json index 6959c5fc0e511..862a00e65215f 100644 --- a/api_docs/licensing.json +++ b/api_docs/licensing.json @@ -3074,7 +3074,7 @@ "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 225 + "lineNumber": 229 } }, { diff --git a/api_docs/security.json b/api_docs/security.json index e0fd2a6bb33f2..f7547221cec66 100644 --- a/api_docs/security.json +++ b/api_docs/security.json @@ -14,7 +14,13 @@ "\nRepresents the currently authenticated user." ], "signature": [ - "AuthenticatedUser", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, " extends ", "User" ], @@ -120,7 +126,13 @@ ], "signature": [ "() => Promise<", - "AuthenticatedUser", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, ">" ], "source": { @@ -943,6 +955,121 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "security", + "id": "def-server.AuditServiceSetup", + "type": "Interface", + "tags": [], + "label": "AuditServiceSetup", + "description": [], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 40 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.AuditServiceSetup.asScoped", + "type": "Function", + "tags": [], + "label": "asScoped", + "description": [], + "signature": [ + "(request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") => ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuditLogger", + "text": "AuditLogger" + } + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 41 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "security", + "id": "def-server.request", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 41 + }, + "deprecated": false + } + ] + }, + { + "parentPluginId": "security", + "id": "def-server.AuditServiceSetup.getLogger", + "type": "Function", + "tags": [], + "label": "getLogger", + "description": [], + "signature": [ + "(id?: string | undefined) => ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.LegacyAuditLogger", + "text": "LegacyAuditLogger" + } + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 42 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "security", + "id": "def-server.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string | undefined" + ], + "source": { + "path": "x-pack/plugins/security/server/audit/audit_service.ts", + "lineNumber": 42 + }, + "deprecated": false + } + ] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "security", "id": "def-server.AuthenticatedUser", @@ -953,7 +1080,13 @@ "\nRepresents the currently authenticated user." ], "signature": [ - "AuthenticatedUser", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, " extends ", "User" ], @@ -1035,6 +1168,174 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "security", + "id": "def-server.AuthenticationServiceStart", + "type": "Interface", + "tags": [], + "label": "AuthenticationServiceStart", + "description": [ + "\nAuthentication services available on the security plugin's start contract." + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 72 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.AuthenticationServiceStart.apiKeys", + "type": "Object", + "tags": [], + "label": "apiKeys", + "description": [], + "signature": [ + "{ create: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", params: ", + "CreateAPIKeyParams", + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.CreateAPIKeyResult", + "text": "CreateAPIKeyResult" + }, + " | null>; areAPIKeysEnabled: () => Promise; invalidate: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", params: ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeysParams", + "text": "InvalidateAPIKeysParams" + }, + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeyResult", + "text": "InvalidateAPIKeyResult" + }, + " | null>; grantAsInternalUser: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ", createParams: ", + "CreateAPIKeyParams", + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.GrantAPIKeyResult", + "text": "GrantAPIKeyResult" + }, + " | null>; invalidateAsInternalUser: (params: ", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeysParams", + "text": "InvalidateAPIKeysParams" + }, + ") => Promise<", + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.InvalidateAPIKeyResult", + "text": "InvalidateAPIKeyResult" + }, + " | null>; }" + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 73 + }, + "deprecated": false + }, + { + "parentPluginId": "security", + "id": "def-server.AuthenticationServiceStart.getCurrentUser", + "type": "Function", + "tags": [], + "label": "getCurrentUser", + "description": [], + "signature": [ + "(request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + ") => ", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, + " | null" + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 81 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "security", + "id": "def-server.request", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "source": { + "path": "x-pack/plugins/security/server/authentication/authentication_service.ts", + "lineNumber": 81 + }, + "deprecated": false + } + ] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "security", "id": "def-server.CheckPrivilegesPayload", @@ -1496,385 +1797,484 @@ } ], "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "security", + "id": "def-server.AuthorizationServiceSetup", + "type": "Type", + "tags": [], + "label": "AuthorizationServiceSetup", + "description": [], + "source": { + "path": "x-pack/plugins/security/server/index.ts", + "lineNumber": 31 + }, + "deprecated": false, + "initialIsOpen": false }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup", - "type": "Interface", + "id": "def-server.ROUTE_TAG_CAN_REDIRECT", + "type": "string", "tags": [], - "label": "SecurityPluginSetup", + "label": "ROUTE_TAG_CAN_REDIRECT", "description": [ - "\nDescribes public Security plugin contract returned at the `setup` stage." + "\nIf the route is marked with this tag Security can safely assume that the calling party that sends\nrequest to this route can handle redirect responses. It's particularly important if we want the\nspecific route to be able to initiate or participate in the authentication handshake that may\ninvolve redirects and will eventually redirect authenticated user to this route." + ], + "signature": [ + "\"security:canRedirect\"" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 69 + "path": "x-pack/plugins/security/server/routes/tags.ts", + "lineNumber": 21 }, "deprecated": false, - "children": [ - { - "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.authc", - "type": "Object", - "tags": [ - "deprecated" - ], - "label": "authc", - "description": [], - "signature": [ - "{ getCurrentUser: (request: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCoreHttpPluginApi", - "section": "def-server.KibanaRequest", - "text": "KibanaRequest" - }, - ") => ", - "AuthenticatedUser", - " | null; }" - ], - "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 73 + "initialIsOpen": false + } + ], + "objects": [], + "setup": { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup", + "type": "Interface", + "tags": [], + "label": "SecurityPluginSetup", + "description": [ + "\nDescribes public Security plugin contract returned at the `setup` stage." + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 72 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.authc", + "type": "Object", + "tags": [ + "deprecated" + ], + "label": "authc", + "description": [], + "signature": [ + "{ getCurrentUser: (request: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" }, - "deprecated": true, - "references": [ - { - "plugin": "reporting", - "link": { - "path": "x-pack/plugins/reporting/server/routes/lib/get_user.ts", - "lineNumber": 13 - } - }, - { - "plugin": "encryptedSavedObjects", - "link": { - "path": "x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts", - "lineNumber": 107 - } - }, - { - "plugin": "encryptedSavedObjects", - "link": { - "path": "x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts", - "lineNumber": 67 - } - }, - { - "plugin": "actions", - "link": { - "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 444 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/routes/annotations.ts", - "lineNumber": 105 - } - }, - { - "plugin": "dashboardMode", - "link": { - "path": "x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts", - "lineNumber": 33 - } - }, - { - "plugin": "dataEnhanced", - "link": { - "path": "x-pack/plugins/data_enhanced/server/search/session/session_service.ts", - "lineNumber": 448 - } - }, - { - "plugin": "logstash", - "link": { - "path": "x-pack/plugins/logstash/server/routes/pipeline/save.ts", - "lineNumber": 41 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts", - "lineNumber": 48 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts", - "lineNumber": 45 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts", - "lineNumber": 45 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts", - "lineNumber": 28 - } + ") => ", + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, + " | null; }" + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 76 + }, + "deprecated": true, + "references": [ + { + "plugin": "reporting", + "link": { + "path": "x-pack/plugins/reporting/server/routes/lib/get_user.ts", + "lineNumber": 13 } - ] + }, + { + "plugin": "encryptedSavedObjects", + "link": { + "path": "x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts", + "lineNumber": 107 + } + }, + { + "plugin": "encryptedSavedObjects", + "link": { + "path": "x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.ts", + "lineNumber": 67 + } + }, + { + "plugin": "actions", + "link": { + "path": "x-pack/plugins/actions/server/plugin.ts", + "lineNumber": 444 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/routes/annotations.ts", + "lineNumber": 105 + } + }, + { + "plugin": "dashboardMode", + "link": { + "path": "x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts", + "lineNumber": 33 + } + }, + { + "plugin": "dataEnhanced", + "link": { + "path": "x-pack/plugins/data_enhanced/server/search/session/session_service.ts", + "lineNumber": 448 + } + }, + { + "plugin": "logstash", + "link": { + "path": "x-pack/plugins/logstash/server/routes/pipeline/save.ts", + "lineNumber": 41 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts", + "lineNumber": 48 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts", + "lineNumber": 45 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts", + "lineNumber": 45 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts", + "lineNumber": 28 + } + } + ] + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.authz", + "type": "Object", + "tags": [ + "deprecated" + ], + "label": "authz", + "description": [], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuthorizationServiceSetup", + "text": "AuthorizationServiceSetup" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 80 }, - { - "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.authz", - "type": "Object", - "tags": [ - "deprecated" - ], - "label": "authz", - "description": [], - "signature": [ - "{ mode: ", - "AuthorizationMode", - "; actions: ", - "Actions", - "; checkPrivilegesDynamicallyWithRequest: ", - "CheckPrivilegesDynamicallyWithRequest", - "; checkPrivilegesWithRequest: ", - "CheckPrivilegesWithRequest", - "; }" - ], - "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 77 + "deprecated": true, + "references": [ + { + "plugin": "actions", + "link": { + "path": "x-pack/plugins/actions/server/plugin.ts", + "lineNumber": 443 + } }, - "deprecated": true, - "references": [ - { - "plugin": "actions", - "link": { - "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 443 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts", - "lineNumber": 54 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/plugin.ts", - "lineNumber": 153 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/server/plugin.ts", - "lineNumber": 203 - } - }, - { - "plugin": "enterpriseSearch", - "link": { - "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", - "lineNumber": 46 - } - }, - { - "plugin": "enterpriseSearch", - "link": { - "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", - "lineNumber": 86 - } - }, - { - "plugin": "enterpriseSearch", - "link": { - "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", - "lineNumber": 88 - } - }, - { - "plugin": "savedObjectsTagging", - "link": { - "path": "x-pack/plugins/saved_objects_tagging/server/request_handler_context.ts", - "lineNumber": 37 - } + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts", + "lineNumber": 54 } - ] + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/plugin.ts", + "lineNumber": 153 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/server/plugin.ts", + "lineNumber": 203 + } + }, + { + "plugin": "enterpriseSearch", + "link": { + "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", + "lineNumber": 46 + } + }, + { + "plugin": "enterpriseSearch", + "link": { + "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", + "lineNumber": 86 + } + }, + { + "plugin": "enterpriseSearch", + "link": { + "path": "x-pack/plugins/enterprise_search/server/lib/check_access.ts", + "lineNumber": 88 + } + }, + { + "plugin": "savedObjectsTagging", + "link": { + "path": "x-pack/plugins/saved_objects_tagging/server/request_handler_context.ts", + "lineNumber": 37 + } + } + ] + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.license", + "type": "Object", + "tags": [], + "label": "license", + "description": [ + "\nExposes information about the available security features under the current license." + ], + "signature": [ + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.SecurityLicense", + "text": "SecurityLicense" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 84 }, + "deprecated": false + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginSetup.audit", + "type": "Object", + "tags": [], + "label": "audit", + "description": [ + "\nExposes services for audit logging." + ], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuditServiceSetup", + "text": "AuditServiceSetup" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 88 + }, + "deprecated": false + } + ], + "lifecycle": "setup", + "initialIsOpen": true + }, + "start": { + "parentPluginId": "security", + "id": "def-server.SecurityPluginStart", + "type": "Interface", + "tags": [], + "label": "SecurityPluginStart", + "description": [ + "\nDescribes public Security plugin contract returned at the `start` stage." + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 94 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginStart.authc", + "type": "Object", + "tags": [], + "label": "authc", + "description": [ + "\nAuthentication services to confirm the user is who they say they are." + ], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuthenticationServiceStart", + "text": "AuthenticationServiceStart" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 98 + }, + "deprecated": false + }, + { + "parentPluginId": "security", + "id": "def-server.SecurityPluginStart.authz", + "type": "Object", + "tags": [], + "label": "authz", + "description": [ + "\nAuthorization services to manage and access the permissions a particular user has." + ], + "signature": [ + { + "pluginId": "security", + "scope": "server", + "docId": "kibSecurityPluginApi", + "section": "def-server.AuthorizationServiceSetup", + "text": "AuthorizationServiceSetup" + } + ], + "source": { + "path": "x-pack/plugins/security/server/plugin.ts", + "lineNumber": 102 + }, + "deprecated": false + } + ], + "lifecycle": "start", + "initialIsOpen": true + } + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [ + { + "parentPluginId": "security", + "id": "def-common.AuthenticatedUser", + "type": "Interface", + "tags": [], + "label": "AuthenticatedUser", + "description": [ + "\nRepresents the currently authenticated user." + ], + "signature": [ + { + "pluginId": "security", + "scope": "common", + "docId": "kibSecurityPluginApi", + "section": "def-common.AuthenticatedUser", + "text": "AuthenticatedUser" + }, + " extends ", + "User" + ], + "source": { + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 21 + }, + "deprecated": false, + "children": [ { "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.license", + "id": "def-common.AuthenticatedUser.authentication_realm", "type": "Object", "tags": [], - "label": "license", - "description": [], + "label": "authentication_realm", + "description": [ + "\nThe name and type of the Realm that has authenticated the user." + ], "signature": [ - { - "pluginId": "security", - "scope": "common", - "docId": "kibSecurityPluginApi", - "section": "def-common.SecurityLicense", - "text": "SecurityLicense" - } + "UserRealm" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 81 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 25 }, "deprecated": false }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginSetup.audit", + "id": "def-common.AuthenticatedUser.lookup_realm", "type": "Object", "tags": [], - "label": "audit", - "description": [], + "label": "lookup_realm", + "description": [ + "\nThe name and type of the Realm where the user information were retrieved from." + ], "signature": [ - "AuditServiceSetup" + "UserRealm" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 82 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 30 }, "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "security", - "id": "def-server.SecurityPluginStart", - "type": "Interface", - "tags": [], - "label": "SecurityPluginStart", - "description": [ - "\nDescribes public Security plugin contract returned at the `start` stage." - ], - "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 88 - }, - "deprecated": false, - "children": [ + }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginStart.authc", + "id": "def-common.AuthenticatedUser.authentication_provider", "type": "Object", "tags": [], - "label": "authc", - "description": [], + "label": "authentication_provider", + "description": [ + "\nThe authentication provider that used to authenticate user." + ], "signature": [ - "{ getCurrentUser: (request: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCoreHttpPluginApi", - "section": "def-server.KibanaRequest", - "text": "KibanaRequest" - }, - ") => ", - "AuthenticatedUser", - " | null; apiKeys: Pick<", - "APIKeys", - ", \"create\" | \"areAPIKeysEnabled\" | \"invalidate\" | \"grantAsInternalUser\" | \"invalidateAsInternalUser\">; }" + "AuthenticationProvider" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 89 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 35 }, "deprecated": false }, { "parentPluginId": "security", - "id": "def-server.SecurityPluginStart.authz", - "type": "Object", + "id": "def-common.AuthenticatedUser.authentication_type", + "type": "string", "tags": [], - "label": "authz", - "description": [], - "signature": [ - "{ mode: ", - "AuthorizationMode", - "; actions: ", - "Actions", - "; checkPrivilegesDynamicallyWithRequest: ", - "CheckPrivilegesDynamicallyWithRequest", - "; checkPrivilegesWithRequest: ", - "CheckPrivilegesWithRequest", - "; }" + "label": "authentication_type", + "description": [ + "\nThe AuthenticationType used by ES to authenticate the user.\n" ], "source": { - "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 90 + "path": "x-pack/plugins/security/common/model/authenticated_user.ts", + "lineNumber": 42 }, "deprecated": false } ], "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "security", - "id": "def-server.AuthorizationServiceSetup", - "type": "Type", - "tags": [], - "label": "AuthorizationServiceSetup", - "description": [], - "signature": [ - "{ mode: ", - "AuthorizationMode", - "; actions: ", - "Actions", - "; checkPrivilegesDynamicallyWithRequest: ", - "CheckPrivilegesDynamicallyWithRequest", - "; checkPrivilegesWithRequest: ", - "CheckPrivilegesWithRequest", - "; }" - ], - "source": { - "path": "x-pack/plugins/security/server/index.ts", - "lineNumber": 30 - }, - "deprecated": false, - "initialIsOpen": false }, - { - "parentPluginId": "security", - "id": "def-server.ROUTE_TAG_CAN_REDIRECT", - "type": "string", - "tags": [], - "label": "ROUTE_TAG_CAN_REDIRECT", - "description": [ - "\nIf the route is marked with this tag Security can safely assume that the calling party that sends\nrequest to this route can handle redirect responses. It's particularly important if we want the\nspecific route to be able to initiate or participate in the authentication handshake that may\ninvolve redirects and will eventually redirect authenticated user to this route." - ], - "signature": [ - "\"security:canRedirect\"" - ], - "source": { - "path": "x-pack/plugins/security/server/routes/tags.ts", - "lineNumber": 21 - }, - "deprecated": false, - "initialIsOpen": false - } - ], - "objects": [] - }, - "common": { - "classes": [], - "functions": [], - "interfaces": [ { "parentPluginId": "security", "id": "def-common.SecurityLicense", diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 07f219ce6e771..250ceff429207 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -27,6 +27,12 @@ import securityObj from './security.json'; ## Server +### Setup + + +### Start + + ### Interfaces diff --git a/api_docs/spaces.json b/api_docs/spaces.json index d24b3884ffac6..002145f3b48db 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1203,7 +1203,7 @@ "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 263 + "lineNumber": 267 } }, { @@ -2002,28 +2002,28 @@ "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 57 + "lineNumber": 60 } }, { "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 263 + "lineNumber": 267 } }, { "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 281 + "lineNumber": 285 } }, { "plugin": "security", "link": { "path": "x-pack/plugins/security/server/plugin.ts", - "lineNumber": 297 + "lineNumber": 301 } }, { diff --git a/docs/api/index-patterns.asciidoc b/docs/api/index-patterns.asciidoc index 79d2f164fb8c3..b343d80b4fc15 100644 --- a/docs/api/index-patterns.asciidoc +++ b/docs/api/index-patterns.asciidoc @@ -20,6 +20,12 @@ The following index patterns APIs are available: ** <> to set a default index pattern * Fields ** <> to change field metadata, such as `count`, `customLabel` and `format` +* Runtime fields + ** <> to retrieve a runtime field + ** <> to create a runtime field + ** <> to create or update a runtime field + ** <> to partially update an existing runtime field + ** <> to delete a runtime field include::index-patterns/get.asciidoc[] include::index-patterns/create.asciidoc[] @@ -28,3 +34,9 @@ include::index-patterns/delete.asciidoc[] include::index-patterns/default-get.asciidoc[] include::index-patterns/default-set.asciidoc[] include::index-patterns/update-fields.asciidoc[] +include::index-patterns/runtime-fields/get.asciidoc[] +include::index-patterns/runtime-fields/create.asciidoc[] +include::index-patterns/runtime-fields/upsert.asciidoc[] +include::index-patterns/runtime-fields/update.asciidoc[] +include::index-patterns/runtime-fields/delete.asciidoc[] + diff --git a/docs/api/index-patterns/create.asciidoc b/docs/api/index-patterns/create.asciidoc index 771292d6f934d..521e25931ad49 100644 --- a/docs/api/index-patterns/create.asciidoc +++ b/docs/api/index-patterns/create.asciidoc @@ -84,6 +84,7 @@ $ curl -X POST api/index_patterns/index_pattern "typeMeta": {}, "fieldFormats": {}, "fieldAttrs": {}, + "runtimeFieldMap": {} "allowNoIndex": "..." } } diff --git a/docs/api/index-patterns/get.asciidoc b/docs/api/index-patterns/get.asciidoc index 3f53bf0726bf1..64588e63f62ae 100644 --- a/docs/api/index-patterns/get.asciidoc +++ b/docs/api/index-patterns/get.asciidoc @@ -58,6 +58,7 @@ The API returns an index pattern object: "typeMeta": {}, "fieldFormats": {}, "fieldAttrs": {}, + "runtimeFieldMap" {}, "allowNoIndex: "..." } } diff --git a/docs/api/index-patterns/runtime-fields/create.asciidoc b/docs/api/index-patterns/runtime-fields/create.asciidoc new file mode 100644 index 0000000000000..b0773c29e5309 --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/create.asciidoc @@ -0,0 +1,61 @@ +[[index-patterns-runtime-field-api-create]] +=== Create runtime field API +++++ +Create runtime field +++++ + +experimental[] Create a runtime field + +[[index-patterns-runtime-field-create-request]] +==== Request + +`POST :/api/index_patterns/index_pattern//runtime_field` + +`POST :/s//api/index_patterns/index_pattern//runtime_field` + +[[index-patterns-runtime-field-create-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +[[index-patterns-runtime-field-create-body]] +==== Request body + +`name`:: (Required, string) The name for a runtime field. + +`runtimeField`:: (Required, object) The runtime field definition object. + + +[[index-patterns-runtime-field-create-example]] +==== Examples + +Create a runtime field on an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern//runtime_field +{ + "name": "runtimeFoo", + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc["foo"].value)" + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns created runtime field object and update index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...}, + "field": {...} +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/runtime-fields/delete.asciidoc b/docs/api/index-patterns/runtime-fields/delete.asciidoc new file mode 100644 index 0000000000000..840789fe1ec23 --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/delete.asciidoc @@ -0,0 +1,37 @@ +[[index-patterns-runtime-field-api-delete]] +=== Delete runtime field API +++++ +Delete runtime field +++++ + +experimental[] Delete a runtime field from an index pattern. + +[[index-patterns-runtime-field-api-delete-request]] +==== Request + +`DELETE :/api/index_patterns/index_pattern//runtime_field/` + +`DELETE :/s//api/index_patterns/index_pattern//runtime_field/` + +[[index-patterns-runtime-field-api-delete-path-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern your want to delete a runtime field from. + +`name`:: +(Required, string) The name of the runtime field you want to delete. + + +==== Example + +Delete a runtime field from an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/index_patterns/index_pattern//runtime_field/ +-------------------------------------------------- +// KIBANA diff --git a/docs/api/index-patterns/runtime-fields/get.asciidoc b/docs/api/index-patterns/runtime-fields/get.asciidoc new file mode 100644 index 0000000000000..42bd209c708bc --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/get.asciidoc @@ -0,0 +1,52 @@ +[[index-patterns-runtime-field-api-get]] +=== Get runtime field API +++++ +Get runtime field +++++ + +experimental[] Get a runtime field + +[[index-patterns-runtime-field-get-request]] +==== Request + +`GET :/api/index_patterns/index_pattern//runtime_field/` + +`GET :/s//api/index_patterns/index_pattern//runtime_field/` + +[[index-patterns-runtime-field-get-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +`name`:: +(Required, string) The name of the runtime field you want to retrieve. + + +[[index-patterns-runtime-field-get-example]] +==== Example + +Retrieve a runtime field named `foo` of index pattern with the `my-pattern` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/index_patterns/index_pattern/my-pattern/runtime_field/foo +-------------------------------------------------- +// KIBANA + +The API returns a runtime `field` object, and a `runtimeField` definition object: + +[source,sh] +-------------------------------------------------- +{ + "field": { + ... + }, + "runtimeField": { + ... + } +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/runtime-fields/update.asciidoc b/docs/api/index-patterns/runtime-fields/update.asciidoc new file mode 100644 index 0000000000000..f34460896f7bc --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/update.asciidoc @@ -0,0 +1,66 @@ +[[index-patterns-runtime-field-api-update]] +=== Update runtime field API +++++ +Update runtime field +++++ + +experimental[] Update an existing runtime field + +[[index-patterns-runtime-field-update-request]] +==== Request + +`POST :/api/index_patterns/index_pattern//runtime_field/` + +`POST :/s//api/index_patterns/index_pattern//runtime_field/` + +[[index-patterns-runtime-field-update-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +`name`:: +(Required, string) The name of the runtime field you want to update. + +[[index-patterns-runtime-field-update-body]] +==== Request body + +`runtimeField`:: (Required, object) The runtime field definition object. + +You can update following fields: + +* `type` +* `script` + + + +[[index-patterns-runtime-field-update-example]] +==== Examples + +Update an existing runtime field on an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern//runtime_field/ +{ + "runtimeField": { + "script": { + "source": "emit(doc["bar"].value)" + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns updated runtime field object and updated index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...}, + "field": {...} +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/runtime-fields/upsert.asciidoc b/docs/api/index-patterns/runtime-fields/upsert.asciidoc new file mode 100644 index 0000000000000..1b436db19c62e --- /dev/null +++ b/docs/api/index-patterns/runtime-fields/upsert.asciidoc @@ -0,0 +1,61 @@ +[[index-patterns-runtime-field-api-upsert]] +=== Upsert runtime field API +++++ +Upsert runtime field +++++ + +experimental[] Create or update an existing runtime field + +[[index-patterns-runtime-field-upsert-request]] +==== Request + +`PUT :/api/index_patterns/index_pattern//runtime_field` + +`PUT :/s//api/index_patterns/index_pattern//runtime_field` + +[[index-patterns-runtime-field-upsert-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`index_pattern_id`:: +(Required, string) The ID of the index pattern. + +[[index-patterns-runtime-field-upsert-body]] +==== Request body + +`name`:: (Required, string) The name for a new runtime field or a name of an existing runtime field. + +`runtimeField`:: (Required, object) The runtime field definition object. + + +[[index-patterns-runtime-field-upsert-example]] +==== Examples + +Create or update an existing runtime field on an index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/index_patterns/index_pattern//runtime_field +{ + "name": "runtimeFoo", + "runtimeField": { + "type": "long", + "script": { + "source": "emit(doc["foo"].value)" + } + } +} +-------------------------------------------------- +// KIBANA + +The API returns created or updated runtime field object and updated index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...}, + "field": {...} +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/update.asciidoc b/docs/api/index-patterns/update.asciidoc index 8ed0ff89fb928..2d5fe882d448d 100644 --- a/docs/api/index-patterns/update.asciidoc +++ b/docs/api/index-patterns/update.asciidoc @@ -93,7 +93,8 @@ $ curl -X POST api/saved_objects/index-pattern/my-pattern "fieldFormats": {}, "type": "...", "typeMeta": {}, - "fields": {} + "fields": {}, + "runtimeFieldMap": {} } } -------------------------------------------------- diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md new file mode 100644 index 0000000000000..c0aca53255b8f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md) + +## IndexPattern.getRuntimeField() method + +Returns runtime field if exists + +Signature: + +```typescript +getRuntimeField(name: string): RuntimeField | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`RuntimeField | null` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md new file mode 100644 index 0000000000000..96dbe13a7f197 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [hasRuntimeField](./kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md) + +## IndexPattern.hasRuntimeField() method + +Checks if runtime field exists + +Signature: + +```typescript +hasRuntimeField(name: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 53d173d39f50d..51ca42fdce70a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -54,13 +54,16 @@ export declare class IndexPattern implements IIndexPattern | [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists | | [getNonScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md) | | | +| [getRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.getruntimefield.md) | | Returns runtime field if exists | | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | +| [hasRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.hasruntimefield.md) | | Checks if runtime field exists | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | -| [removeRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [replaceAllRuntimeFields(newFields)](./kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md) | | Replaces all existing runtime fields with new fields | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-public.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcount.md) | | | | [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-public.indexpattern.setfieldcustomlabel.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md index 7a5228fece782..f2774924fc73c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removeruntimefield.md @@ -4,7 +4,7 @@ ## IndexPattern.removeRuntimeField() method -Remove a runtime field - removed from mapped field or removed unmapped field as appropriate +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. Signature: @@ -16,7 +16,7 @@ removeRuntimeField(name: string): void; | Parameter | Type | Description | | --- | --- | --- | -| name | string | | +| name | string | Field name to remove | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md new file mode 100644 index 0000000000000..076b2b38cf474 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [replaceAllRuntimeFields](./kibana-plugin-plugins-data-public.indexpattern.replaceallruntimefields.md) + +## IndexPattern.replaceAllRuntimeFields() method + +Replaces all existing runtime fields with new fields + +Signature: + +```typescript +replaceAllRuntimeFields(newFields: Record): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newFields | Record<string, RuntimeField> | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md new file mode 100644 index 0000000000000..d5dc8f966316b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md) + +## IndexPattern.getRuntimeField() method + +Returns runtime field if exists + +Signature: + +```typescript +getRuntimeField(name: string): RuntimeField | null; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`RuntimeField | null` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md new file mode 100644 index 0000000000000..5000d5e645cbb --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [hasRuntimeField](./kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md) + +## IndexPattern.hasRuntimeField() method + +Checks if runtime field exists + +Signature: + +```typescript +hasRuntimeField(name: string): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 97d1cd9115262..27b8a31a2582b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -54,13 +54,16 @@ export declare class IndexPattern implements IIndexPattern | [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getFormatterForFieldNoDefault(fieldname)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfieldnodefault.md) | | Get formatter for a given field name. Return undefined if none exists | | [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | | +| [getRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.getruntimefield.md) | | Returns runtime field if exists | | [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | | | [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | +| [hasRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.hasruntimefield.md) | | Checks if runtime field exists | | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | -| [removeRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate | +| [removeRuntimeField(name)](./kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md) | | Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [replaceAllRuntimeFields(newFields)](./kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md) | | Replaces all existing runtime fields with new fields | | [setFieldAttrs(fieldName, attrName, value)](./kibana-plugin-plugins-data-server.indexpattern.setfieldattrs.md) | | | | [setFieldCount(fieldName, count)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcount.md) | | | | [setFieldCustomLabel(fieldName, customLabel)](./kibana-plugin-plugins-data-server.indexpattern.setfieldcustomlabel.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md index da8e7e40a7fac..ef32b80ba8502 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removeruntimefield.md @@ -4,7 +4,7 @@ ## IndexPattern.removeRuntimeField() method -Remove a runtime field - removed from mapped field or removed unmapped field as appropriate +Remove a runtime field - removed from mapped field or removed unmapped field as appropriate. Doesn't clear associated field attributes. Signature: @@ -16,7 +16,7 @@ removeRuntimeField(name: string): void; | Parameter | Type | Description | | --- | --- | --- | -| name | string | | +| name | string | Field name to remove | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md new file mode 100644 index 0000000000000..35df871763f8a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [replaceAllRuntimeFields](./kibana-plugin-plugins-data-server.indexpattern.replaceallruntimefields.md) + +## IndexPattern.replaceAllRuntimeFields() method + +Replaces all existing runtime fields with new fields + +Signature: + +```typescript +replaceAllRuntimeFields(newFields: Record): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newFields | Record<string, RuntimeField> | | + +Returns: + +`void` + diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 36d613ec82f9e..d4ab8f624f711 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -32,14 +32,14 @@ export class DocLinksService { guide: `${KIBANA_DOCS}canvas.html`, }, dashboard: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, - drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldowns`, - urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, + guide: `${KIBANA_DOCS}dashboard.html`, + drilldowns: `${KIBANA_DOCS}drilldowns.html`, + drilldownsTriggerPicker: `${KIBANA_DOCS}drilldowns.html#url-drilldowns`, + urlDrilldownTemplateSyntax: `${KIBANA_DOCS}url_templating-language.html`, + urlDrilldownVariables: `${KIBANA_DOCS}url_templating-language.html#url-template-variables`, }, discover: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/discover.html`, + guide: `${KIBANA_DOCS}discover.html`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, @@ -128,14 +128,14 @@ export class DocLinksService { luceneExpressions: `${ELASTICSEARCH_DOCS}modules-scripting-expression.html`, }, indexPatterns: { - introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, - fieldFormattersNumber: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/numeral.html`, - fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, + introduction: `${KIBANA_DOCS}index-patterns.html`, + fieldFormattersNumber: `${KIBANA_DOCS}numeral.html`, + fieldFormattersString: `${KIBANA_DOCS}field-formatters-string.html`, runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, }, - addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, - kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, - upgradeAssistant: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/upgrade-assistant.html`, + addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, + kibana: `${KIBANA_DOCS}index.html`, + upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, @@ -195,23 +195,23 @@ export class DocLinksService { }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, - kueryQuerySyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kuery-query.html`, + kueryQuerySyntax: `${KIBANA_DOCS}kuery-query.html`, luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`, percolate: `${ELASTICSEARCH_DOCS}query-dsl-percolate-query.html`, queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`, }, search: { - sessions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/search-sessions.html`, + sessions: `${KIBANA_DOCS}search-sessions.html`, }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, }, management: { - dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, + dashboardSettings: `${KIBANA_DOCS}advanced-options.html#kibana-dashboard-settings`, indexManagement: `${ELASTICSEARCH_DOCS}index-mgmt.html`, - kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, - visualizationSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-visualization-settings`, + kibanaSearchSettings: `${KIBANA_DOCS}advanced-options.html#kibana-search-settings`, + visualizationSettings: `${KIBANA_DOCS}advanced-options.html#kibana-visualization-settings`, }, ml: { guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, @@ -242,52 +242,52 @@ export class DocLinksService { guide: `${ELASTICSEARCH_DOCS}transforms.html`, }, visualize: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, - timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/timelion.html`, + guide: `${KIBANA_DOCS}dashboard.html`, + timelionDeprecation: `${KIBANA_DOCS}timelion.html`, lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, - lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, + lensPanels: `${KIBANA_DOCS}lens.html`, maps: `${ELASTIC_WEBSITE_URL}maps`, - vega: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/vega.html`, + vega: `${KIBANA_DOCS}vega.html`, }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, }, alerting: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-management.html`, - actionTypes: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/action-types.html`, - emailAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, - emailActionConfig: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, - generalSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`, - indexAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html`, - esQuery: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-es-query.html`, - indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-index-threshold.html`, - pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, - preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-connectors.html`, - preconfiguredAlertHistoryConnector: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html#preconfigured-connector-alert-history`, - serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`, - setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, - slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`, - teamsAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`, + guide: `${KIBANA_DOCS}alert-management.html`, + actionTypes: `${KIBANA_DOCS}action-types.html`, + emailAction: `${KIBANA_DOCS}email-action-type.html`, + emailActionConfig: `${KIBANA_DOCS}email-action-type.html`, + generalSettings: `${KIBANA_DOCS}alert-action-settings-kb.html#general-alert-action-settings`, + indexAction: `${KIBANA_DOCS}index-action-type.html`, + esQuery: `${KIBANA_DOCS}rule-type-es-query.html`, + indexThreshold: `${KIBANA_DOCS}rule-type-index-threshold.html`, + pagerDutyAction: `${KIBANA_DOCS}pagerduty-action-type.html`, + preconfiguredConnectors: `${KIBANA_DOCS}pre-configured-connectors.html`, + preconfiguredAlertHistoryConnector: `${KIBANA_DOCS}index-action-type.html#preconfigured-connector-alert-history`, + serviceNowAction: `${KIBANA_DOCS}servicenow-action-type.html#configuring-servicenow`, + setupPrerequisites: `${KIBANA_DOCS}alerting-getting-started.html#alerting-setup-prerequisites`, + slackAction: `${KIBANA_DOCS}slack-action-type.html#configuring-slack`, + teamsAction: `${KIBANA_DOCS}teams-action-type.html#configuring-teams`, }, maps: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, - importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, + guide: `${KIBANA_DOCS}maps.html`, + importGeospatialPrivileges: `${KIBANA_DOCS}import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { - alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, - alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`, - alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, - alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, - alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, - alertsKibanaThreadpoolRejections: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-thread-pool-rejections`, - alertsKibanaCCRReadExceptions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, - alertsKibanaLargeShardSize: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-large-shard-size`, - alertsKibanaClusterAlerts: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cluster-alerts`, + alertsKibana: `${KIBANA_DOCS}kibana-alerts.html`, + alertsKibanaCpuThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-cpu-threshold`, + alertsKibanaDiskThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-disk-usage-threshold`, + alertsKibanaJvmThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, + alertsKibanaMissingData: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-missing-monitoring-data`, + alertsKibanaThreadpoolRejections: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-thread-pool-rejections`, + alertsKibanaCCRReadExceptions: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, + alertsKibanaLargeShardSize: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-large-shard-size`, + alertsKibanaClusterAlerts: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-cluster-alerts`, metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, - monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, + monitorKibana: `${KIBANA_DOCS}monitoring-metricbeat.html`, monitorLogstash: `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html`, - troubleshootKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitor-troubleshooting.html`, + troubleshootKibana: `${KIBANA_DOCS}monitor-troubleshooting.html`, }, security: { apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, @@ -295,8 +295,8 @@ export class DocLinksService { elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, - kibanaTLS: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`, - kibanaPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-privileges.html`, + kibanaTLS: `${KIBANA_DOCS}configuring-tls.html`, + kibanaPrivileges: `${KIBANA_DOCS}kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, runAsPrivilege: `${ELASTICSEARCH_DOCS}security-privileges.html#_run_as_privilege`, @@ -305,7 +305,7 @@ export class DocLinksService { jiraAction: `${ELASTICSEARCH_DOCS}actions-jira.html`, pagerDutyAction: `${ELASTICSEARCH_DOCS}actions-pagerduty.html`, slackAction: `${ELASTICSEARCH_DOCS}actions-slack.html`, - ui: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/watcher-ui.html`, + ui: `${KIBANA_DOCS}watcher-ui.html`, }, ccs: { guide: `${ELASTICSEARCH_DOCS}modules-cross-cluster-search.html`, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index c897cdbce2309..0c3a9901f8c8c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -364,7 +364,6 @@ export class IndexPattern implements IIndexPattern { * @param name Field name * @param runtimeField Runtime field definition */ - addRuntimeField(name: string, runtimeField: RuntimeField) { const existingField = this.getFieldByName(name); if (existingField) { @@ -384,11 +383,41 @@ export class IndexPattern implements IIndexPattern { } /** - * Remove a runtime field - removed from mapped field or removed unmapped - * field as appropriate - * @param name Field name + * Checks if runtime field exists + * @param name + */ + hasRuntimeField(name: string): boolean { + return !!this.runtimeFieldMap[name]; + } + + /** + * Returns runtime field if exists + * @param name + */ + getRuntimeField(name: string): RuntimeField | null { + return this.runtimeFieldMap[name] ?? null; + } + + /** + * Replaces all existing runtime fields with new fields + * @param newFields */ + replaceAllRuntimeFields(newFields: Record) { + const oldRuntimeFieldNames = Object.keys(this.runtimeFieldMap); + oldRuntimeFieldNames.forEach((name) => { + this.removeRuntimeField(name); + }); + Object.entries(newFields).forEach(([name, field]) => { + this.addRuntimeField(name, field); + }); + } + + /** + * Remove a runtime field - removed from mapped field or removed unmapped + * field as appropriate. Doesn't clear associated field attributes. + * @param name - Field name to remove + */ removeRuntimeField(name: string) { const existingField = this.getFieldByName(name); if (existingField) { @@ -396,9 +425,6 @@ export class IndexPattern implements IIndexPattern { // mapped field, remove runtimeField def existingField.runtimeField = undefined; } else { - // runtimeField only - this.setFieldCustomLabel(name, null); - this.deleteFieldFormat(name); this.fields.remove(existingField); } } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5ef499840fa6d..67534577d99fc 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1413,6 +1413,7 @@ export class IndexPattern implements IIndexPattern { typeMeta?: string | undefined; type?: string | undefined; }; + getRuntimeField(name: string): RuntimeField | null; // @deprecated (undocumented) getScriptedFields(): IndexPatternField[]; getSourceFiltering(): { @@ -1420,6 +1421,7 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getTimeField(): IndexPatternField | undefined; + hasRuntimeField(name: string): boolean; // (undocumented) id?: string; // @deprecated (undocumented) @@ -1433,6 +1435,7 @@ export class IndexPattern implements IIndexPattern { removeRuntimeField(name: string): void; // @deprecated removeScriptedField(fieldName: string): void; + replaceAllRuntimeFields(newFields: Record): void; resetOriginalSavedObjectBody: () => void; // (undocumented) protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 9bff590b54f1c..d2d8cb82cf646 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -21,6 +21,11 @@ import { registerDeleteScriptedFieldRoute } from './routes/scripted_fields/delet import { registerUpdateScriptedFieldRoute } from './routes/scripted_fields/update_scripted_field'; import type { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { registerManageDefaultIndexPatternRoutes } from './routes/default_index_pattern'; +import { registerCreateRuntimeFieldRoute } from './routes/runtime_fields/create_runtime_field'; +import { registerGetRuntimeFieldRoute } from './routes/runtime_fields/get_runtime_field'; +import { registerDeleteRuntimeFieldRoute } from './routes/runtime_fields/delete_runtime_field'; +import { registerPutRuntimeFieldRoute } from './routes/runtime_fields/put_runtime_field'; +import { registerUpdateRuntimeFieldRoute } from './routes/runtime_fields/update_runtime_field'; export function registerRoutes( http: HttpServiceSetup, @@ -55,6 +60,13 @@ export function registerRoutes( registerDeleteScriptedFieldRoute(router, getStartServices); registerUpdateScriptedFieldRoute(router, getStartServices); + // Runtime Fields API + registerCreateRuntimeFieldRoute(router, getStartServices); + registerGetRuntimeFieldRoute(router, getStartServices); + registerDeleteRuntimeFieldRoute(router, getStartServices); + registerPutRuntimeFieldRoute(router, getStartServices); + registerUpdateRuntimeFieldRoute(router, getStartServices); + router.get( { path: '/api/index_patterns/_fields_for_wildcard', diff --git a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts index d076733462622..7049903f84e8c 100644 --- a/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts +++ b/src/plugins/data/server/index_patterns/routes/create_index_pattern.ts @@ -9,7 +9,11 @@ import { schema } from '@kbn/config-schema'; import { IndexPatternSpec } from 'src/plugins/data/common'; import { handleErrors } from './util/handle_errors'; -import { fieldSpecSchema, serializedFieldFormatSchema } from './util/schemas'; +import { + fieldSpecSchema, + runtimeFieldSpecSchema, + serializedFieldFormatSchema, +} from './util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; @@ -39,6 +43,7 @@ const indexPatternSpecSchema = schema.object({ ) ), allowNoIndex: schema.maybe(schema.boolean()), + runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSpecSchema)), }); export const registerCreateIndexPatternRoute = ( @@ -66,6 +71,7 @@ export const registerCreateIndexPatternRoute = ( elasticsearchClient ); const body = req.body; + const indexPattern = await indexPatternsService.createAndSave( body.index_pattern as IndexPatternSpec, body.override, diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts new file mode 100644 index 0000000000000..faf6d87b6d10b --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/create_runtime_field.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { runtimeFieldSpecSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerCreateRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + body: schema.object({ + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + runtimeField: runtimeFieldSpecSchema, + }), + }, + }, + + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { name, runtimeField } = req.body; + + const indexPattern = await indexPatternsService.get(id); + + if (indexPattern.fields.getByName(name)) { + throw new Error(`Field [name = ${name}] already exists.`); + } + + indexPattern.addRuntimeField(name, runtimeField); + + const addedField = indexPattern.fields.getByName(name); + if (!addedField) throw new Error(`Could not create a field [name = ${name}].`); + + await indexPatternsService.updateSavedObject(indexPattern); + + const savedField = indexPattern.fields.getByName(name); + if (!savedField) throw new Error(`Could not create a field [name = ${name}].`); + + return res.ok({ + body: { + field: savedField.toSpec(), + index_pattern: indexPattern.toSpec(), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts new file mode 100644 index 0000000000000..58b8529d7cf5a --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/delete_runtime_field.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerDeleteRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.delete( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field/{name}', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + + const indexPattern = await indexPatternsService.get(id); + const field = indexPattern.fields.getByName(name); + + if (!field) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!field.runtimeField) { + throw new Error('Only runtime fields can be deleted.'); + } + + indexPattern.removeRuntimeField(name); + + await indexPatternsService.updateSavedObject(indexPattern); + + return res.ok(); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts new file mode 100644 index 0000000000000..6bc2bf396c0b4 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/get_runtime_field.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerGetRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.get( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field/{name}', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + }, + }, + + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + + const indexPattern = await indexPatternsService.get(id); + + const field = indexPattern.fields.getByName(name); + + if (!field) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + if (!field.runtimeField) { + throw new Error('Only runtime fields can be retrieved.'); + } + + return res.ok({ + body: { + field: field.toSpec(), + runtimeField: indexPattern.getRuntimeField(name), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts new file mode 100644 index 0000000000000..a5e92fa5a36ec --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/put_runtime_field.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { handleErrors } from '../util/handle_errors'; +import { runtimeFieldSpecSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerPutRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.put( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + body: schema.object({ + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + runtimeField: runtimeFieldSpecSchema, + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const { name, runtimeField } = req.body; + + const indexPattern = await indexPatternsService.get(id); + + const oldFieldObject = indexPattern.fields.getByName(name); + + if (oldFieldObject && !oldFieldObject.runtimeField) { + throw new Error('Only runtime fields can be updated'); + } + + if (oldFieldObject) { + indexPattern.removeRuntimeField(name); + } + + indexPattern.addRuntimeField(name, runtimeField); + + await indexPatternsService.updateSavedObject(indexPattern); + + const fieldObject = indexPattern.fields.getByName(name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${name}].`); + + return res.ok({ + body: { + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts b/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts new file mode 100644 index 0000000000000..3f3aae46c4388 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/runtime_fields/update_runtime_field.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { RuntimeField } from 'src/plugins/data/common'; +import { ErrorIndexPatternFieldNotFound } from '../../error'; +import { handleErrors } from '../util/handle_errors'; +import { runtimeFieldSpec, runtimeFieldSpecTypeSchema } from '../util/schemas'; +import { IRouter, StartServicesAccessor } from '../../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../../plugin'; + +export const registerUpdateRuntimeFieldRoute = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.post( + { + path: '/api/index_patterns/index_pattern/{id}/runtime_field/{name}', + validate: { + params: schema.object({ + id: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + name: schema.string({ + minLength: 1, + maxLength: 1_000, + }), + }), + body: schema.object({ + name: schema.never(), + runtimeField: schema.object({ + ...runtimeFieldSpec, + // We need to overwrite the below fields on top of `runtimeFieldSpec`, + // because some fields would be optional + type: schema.maybe(runtimeFieldSpecTypeSchema), + }), + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + const id = req.params.id; + const name = req.params.name; + const runtimeField = req.body.runtimeField as Partial; + + const indexPattern = await indexPatternsService.get(id); + const existingRuntimeField = indexPattern.getRuntimeField(name); + + if (!existingRuntimeField) { + throw new ErrorIndexPatternFieldNotFound(id, name); + } + + indexPattern.removeRuntimeField(name); + indexPattern.addRuntimeField(name, { + ...existingRuntimeField, + ...runtimeField, + }); + + await indexPatternsService.updateSavedObject(indexPattern); + + const fieldObject = indexPattern.fields.getByName(name); + if (!fieldObject) throw new Error(`Could not create a field [name = ${name}].`); + + return res.ok({ + body: { + field: fieldObject.toSpec(), + index_pattern: indexPattern.toSpec(), + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts index c1509b9b848be..1c88550c154c5 100644 --- a/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts +++ b/src/plugins/data/server/index_patterns/routes/update_index_pattern.ts @@ -8,7 +8,11 @@ import { schema } from '@kbn/config-schema'; import { handleErrors } from './util/handle_errors'; -import { fieldSpecSchema, serializedFieldFormatSchema } from './util/schemas'; +import { + fieldSpecSchema, + runtimeFieldSpecSchema, + serializedFieldFormatSchema, +} from './util/schemas'; import { IRouter, StartServicesAccessor } from '../../../../../core/server'; import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; @@ -28,6 +32,7 @@ const indexPatternUpdateSchema = schema.object({ fieldFormats: schema.maybe(schema.recordOf(schema.string(), serializedFieldFormatSchema)), fields: schema.maybe(schema.recordOf(schema.string(), fieldSpecSchema)), allowNoIndex: schema.maybe(schema.boolean()), + runtimeFieldMap: schema.maybe(schema.recordOf(schema.string(), runtimeFieldSpecSchema)), }); export const registerUpdateIndexPatternRoute = ( @@ -78,6 +83,7 @@ export const registerUpdateIndexPatternRoute = ( type, typeMeta, fields, + runtimeFieldMap, }, } = req.body; @@ -131,6 +137,11 @@ export const registerUpdateIndexPatternRoute = ( ); } + if (runtimeFieldMap !== undefined) { + changeCount++; + indexPattern.replaceAllRuntimeFields(runtimeFieldMap); + } + if (changeCount < 1) { throw new Error('Index pattern change set is empty.'); } diff --git a/src/plugins/data/server/index_patterns/routes/util/schemas.ts b/src/plugins/data/server/index_patterns/routes/util/schemas.ts index d916423c4fc69..79ee1ffa1ab97 100644 --- a/src/plugins/data/server/index_patterns/routes/util/schemas.ts +++ b/src/plugins/data/server/index_patterns/routes/util/schemas.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { schema } from '@kbn/config-schema'; +import { schema, Type } from '@kbn/config-schema'; +import { RUNTIME_FIELD_TYPES, RuntimeType } from '../../../../common'; export const serializedFieldFormatSchema = schema.object({ id: schema.maybe(schema.string()), @@ -52,4 +53,24 @@ export const fieldSpecSchemaFields = { shortDotsEnable: schema.maybe(schema.boolean()), }; -export const fieldSpecSchema = schema.object(fieldSpecSchemaFields); +export const fieldSpecSchema = schema.object(fieldSpecSchemaFields, { + // Allow and ignore unknowns to make fields transient. + // Because `fields` have a bunch of calculated fields + // this allows to retrieve an index pattern and then to re-create by using the retrieved payload + unknowns: 'ignore', +}); + +export const runtimeFieldSpecTypeSchema = schema.oneOf( + RUNTIME_FIELD_TYPES.map((runtimeFieldType) => schema.literal(runtimeFieldType)) as [ + Type + ] +); +export const runtimeFieldSpec = { + type: runtimeFieldSpecTypeSchema, + script: schema.maybe( + schema.object({ + source: schema.string(), + }) + ), +}; +export const runtimeFieldSpecSchema = schema.object(runtimeFieldSpec); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ff265ccf53301..783bd8d2fcd0e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -816,6 +816,7 @@ export class IndexPattern implements IIndexPattern { typeMeta?: string | undefined; type?: string | undefined; }; + getRuntimeField(name: string): RuntimeField | null; // @deprecated (undocumented) getScriptedFields(): IndexPatternField[]; getSourceFiltering(): { @@ -823,6 +824,7 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getTimeField(): IndexPatternField | undefined; + hasRuntimeField(name: string): boolean; // (undocumented) id?: string; // @deprecated (undocumented) @@ -836,6 +838,7 @@ export class IndexPattern implements IIndexPattern { removeRuntimeField(name: string): void; // @deprecated removeScriptedField(fieldName: string): void; + replaceAllRuntimeFields(newFields: Record): void; resetOriginalSavedObjectBody: () => void; // (undocumented) protected setFieldAttrs(fieldName: string, attrName: K, value: FieldAttrSet[K]): void; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index ce987e2870466..0430614d413b6 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -346,6 +346,7 @@ export function DiscoverLayout({ verticalPosition={contentCentered ? 'center' : undefined} horizontalPosition={contentCentered ? 'center' : undefined} paddingSize="none" + hasShadow={false} className={classNames('dscPageContent', { 'dscPageContent--centered': contentCentered, })} diff --git a/src/plugins/discover/public/application/helpers/columns.test.ts b/src/plugins/discover/public/application/helpers/columns.test.ts index df3884ab98bcf..6b6125193b5f3 100644 --- a/src/plugins/discover/public/application/helpers/columns.test.ts +++ b/src/plugins/discover/public/application/helpers/columns.test.ts @@ -43,4 +43,9 @@ describe('getDisplayedColumns', () => { ] `); }); + test('returns the same instance of ["_source"] over multiple calls', async () => { + const result = getDisplayedColumns([], indexPatternWithTimefieldMock); + const result2 = getDisplayedColumns([], indexPatternWithTimefieldMock); + expect(result).toBe(result2); + }); }); diff --git a/src/plugins/discover/public/application/helpers/columns.ts b/src/plugins/discover/public/application/helpers/columns.ts index 426059060f329..6e77717c5cf05 100644 --- a/src/plugins/discover/public/application/helpers/columns.ts +++ b/src/plugins/discover/public/application/helpers/columns.ts @@ -8,6 +8,11 @@ import { IndexPattern } from '../../../../data/common'; +// We store this outside the function as a constant, so we're not creating a new array every time +// the function is returning this. A changing array might cause the data grid to think it got +// new columns, and thus performing worse than using the same array over multiple renders. +const SOURCE_ONLY = ['_source']; + /** * Function to provide fallback when * 1) no columns are given @@ -19,5 +24,5 @@ export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: I // check if all columns where removed except the configured timeField (this can't be removed) !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName) ? stateColumns - : ['_source']; + : SOURCE_ONLY; } diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap index 645694371f905..1310488c65fab 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -59,7 +59,6 @@ exports[`EmptyState should render normally 1`] = ` } - isDisabled={false} onClick={[Function]} title={ { docLinks={docLinks} onRefresh={() => {}} navigateToApp={async () => {}} - getMlCardState={() => MlCardState.ENABLED} canSave={true} /> ); @@ -48,7 +46,6 @@ describe('EmptyState', () => { docLinks={docLinks} onRefresh={onRefreshHandler} navigateToApp={async () => {}} - getMlCardState={() => MlCardState.ENABLED} canSave={true} /> ); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx index 438eb8a031993..240e732752916 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -8,7 +8,6 @@ import './empty_state.scss'; import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocLinksStart, ApplicationStart } from 'kibana/public'; import { @@ -28,60 +27,18 @@ import { } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public'; -import { MlCardState } from '../../../types'; export const EmptyState = ({ onRefresh, navigateToApp, docLinks, - getMlCardState, canSave, }: { onRefresh: () => void; navigateToApp: ApplicationStart['navigateToApp']; docLinks: DocLinksStart; - getMlCardState: () => MlCardState; canSave: boolean; }) => { - const mlCard = ( - - navigateToApp('ml', { path: '#/filedatavisualizer' })} - className="inpEmptyState__card" - betaBadgeLabel={ - getMlCardState() === MlCardState.ENABLED - ? undefined - : i18n.translate( - 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel', - { - defaultMessage: 'Basic', - } - ) - } - betaBadgeTooltipContent={i18n.translate( - 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription', - { - defaultMessage: 'This feature requires a Basic license.', - } - )} - isDisabled={getMlCardState() === MlCardState.DISABLED} - icon={} - title={ - - } - description={ - - } - /> - - ); - const createAnyway = ( - {getMlCardState() !== MlCardState.HIDDEN ? mlCard : <>} + + navigateToApp('home', { path: '#/tutorial_directory/fileDataViz' })} + className="inpEmptyState__card" + icon={} + title={ + + } + description={ + + } + /> + { application, http, data, - getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); const [creationOptions, setCreationOptions] = useState([]); @@ -182,7 +181,6 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { onRefresh={loadSources} docLinks={docLinks} navigateToApp={application.navigateToApp} - getMlCardState={getMlCardState} canSave={canSave} /> ); diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 94611705a9390..726c055d1b8c3 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -30,5 +30,3 @@ export { IndexPatternCreationOption, IndexPatternListConfig, } from './service'; - -export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 355f529fe0f75..ec5b7c74020a5 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -23,7 +23,7 @@ import { CreateIndexPatternWizardWithRouter, } from '../components'; import { IndexPatternManagementStartDependencies, IndexPatternManagementStart } from '../plugin'; -import { IndexPatternManagmentContext, MlCardState } from '../types'; +import { IndexPatternManagmentContext } from '../types'; const readOnlyBadge = { text: i18n.translate('indexPatternManagement.indexPatterns.badge.readOnly.text', { @@ -37,8 +37,7 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, - params: ManagementAppMountParams, - getMlCardState: () => MlCardState + params: ManagementAppMountParams ) { const [ { chrome, application, uiSettings, notifications, overlays, http, docLinks }, @@ -63,7 +62,6 @@ export async function mountManagementSection( indexPatternFieldEditor, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, - getMlCardState, fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 3462131e50463..6c709fb14f08d 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -26,9 +26,6 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ list: { addListConfig: jest.fn(), } as any, - environment: { - update: jest.fn(), - }, }); const createStartContract = (): IndexPatternManagementStart => ({ @@ -93,7 +90,6 @@ const createIndexPatternManagmentContext = (): { indexPatternFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, - getMlCardState: () => 2, fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; }; diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ed92172c8b91c..e3c156927bfac 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -77,9 +77,7 @@ export class IndexPatternManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params, () => - this.indexPatternManagementService.environmentService.getEnvironment().ml() - ); + return mountManagementSection(core.getStartServices, params); }, }); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts deleted file mode 100644 index 1eaab2eaccc11..0000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { EnvironmentService, EnvironmentServiceSetup } from './environment'; -import { MlCardState } from '../../types'; - -const createSetupMock = (): jest.Mocked => { - const setup = { - update: jest.fn(), - }; - return setup; -}; - -const createMock = (): jest.Mocked> => { - const service = { - setup: jest.fn(), - getEnvironment: jest.fn(() => ({ - ml: () => MlCardState.ENABLED, - })), - }; - service.setup.mockImplementation(createSetupMock); - return service; -}; - -export const environmentServiceMock = { - createSetup: createSetupMock, - create: createMock, -}; diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts deleted file mode 100644 index 9e571374b4784..0000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EnvironmentService } from './environment'; -import { MlCardState } from '../../types'; - -describe('EnvironmentService', () => { - describe('setup', () => { - test('allows multiple update calls', () => { - const setup = new EnvironmentService().setup(); - expect(() => { - setup.update({ ml: () => MlCardState.ENABLED }); - }).not.toThrow(); - }); - }); - - describe('getEnvironment', () => { - test('returns default values', () => { - const service = new EnvironmentService(); - expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); - }); - - test('returns last state of update calls', () => { - let cardState = MlCardState.DISABLED; - const service = new EnvironmentService(); - const setup = service.setup(); - setup.update({ ml: () => cardState }); - expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); - cardState = MlCardState.ENABLED; - expect(service.getEnvironment().ml()).toEqual(MlCardState.ENABLED); - }); - }); -}); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.ts b/src/plugins/index_pattern_management/public/service/environment/environment.ts deleted file mode 100644 index 7bf0c1eb52068..0000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { MlCardState } from '../../types'; - -/** @public */ -export interface Environment { - /** - * Flag whether ml features should be advertised - */ - readonly ml: () => MlCardState; -} - -export class EnvironmentService { - private environment = { - ml: () => MlCardState.DISABLED, - }; - - public setup() { - return { - /** - * Update the environment to influence how available features are presented. - * @param update - */ - update: (update: Partial) => { - this.environment = Object.assign({}, this.environment, update); - }, - }; - } - - public getEnvironment() { - return this.environment; - } -} - -export type EnvironmentServiceSetup = ReturnType; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index 15be7f11892e4..f30ccfcb9f3ed 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -9,7 +9,6 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; -import { EnvironmentService } from './environment'; interface SetupDependencies { httpClient: HttpSetup; } @@ -22,12 +21,10 @@ interface SetupDependencies { export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; - environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); - this.environmentService = new EnvironmentService(); } public setup({ httpClient }: SetupDependencies) { @@ -40,7 +37,6 @@ export class IndexPatternManagementService { return { creation: creationManagerSetup, list: indexPatternListConfigSetup, - environment: this.environmentService.setup(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 58a138df633fd..a61eeb99b25a5 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -33,14 +33,7 @@ export interface IndexPatternManagmentContext { indexPatternFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; - getMlCardState: () => MlCardState; fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; } export type IndexPatternManagmentContextValue = KibanaReactContextValue; - -export enum MlCardState { - HIDDEN, - DISABLED, - ENABLED, -} diff --git a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts index 3bec22de48ee6..16861f3c28051 100644 --- a/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts +++ b/test/api_integration/apis/index_patterns/fields_api/update_fields/main.ts @@ -22,6 +22,7 @@ export default function ({ getService }: FtrProviderContext) { indexPattern = ( await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title: basicIndex, }, diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js index 9c1e1bba0ab9a..656b4e506fa23 100644 --- a/test/api_integration/apis/index_patterns/index.js +++ b/test/api_integration/apis/index_patterns/index.js @@ -15,5 +15,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./scripted_fields_crud')); loadTestFile(require.resolve('./fields_api')); loadTestFile(require.resolve('./default_index_pattern')); + loadTestFile(require.resolve('./runtime_fields_crud')); + loadTestFile(require.resolve('./integration')); }); } diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts index 31c3f13a6e05f..500a642f60850 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/main.ts @@ -117,7 +117,7 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body.index_pattern.fields.bar.type).to.be('boolean'); }); - it('Can add scripted fields, other fields created from es index', async () => { + it('can add scripted fields, other fields created from es index', async () => { const title = `basic_index*`; const response = await supertest.post('/api/index_patterns/index_pattern').send({ override: true, @@ -159,6 +159,32 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body.index_pattern.fields.bar.esTypes[0]).to.be('test-type'); expect(response.body.index_pattern.fields.bar.scripted).to.be(true); }); + + it('can add runtime fields', async () => { + const title = `basic_index*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body.index_pattern.title).to.be(title); + + expect(response.body.index_pattern.runtimeFieldMap.runtimeFoo.type).to.be('keyword'); + expect(response.body.index_pattern.runtimeFieldMap.runtimeFoo.script.source).to.be( + 'emit(doc["foo"].value)' + ); + }); }); it('can specify optional typeMeta attribute when creating an index pattern', async () => { diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts index 2f62ea231b722..598001644eedb 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/create_index_pattern/validation.ts @@ -64,5 +64,25 @@ export default function ({ getService }: FtrProviderContext) { '[request body.refresh_fields]: expected value of type [boolean] but got [number]' ); }); + + it('returns an error when unknown runtime field type', async () => { + const title = `basic_index*`; + const response = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'wrong-type', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response.status).to.be(400); + }); }); } diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts index cd34724e6cda3..7532278d7eb12 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/update_index_pattern/main.ts @@ -284,5 +284,53 @@ export default function ({ getService }: FtrProviderContext) { expect(response3.body.index_pattern.intervalName).to.be('intervalName2'); expect(response3.body.index_pattern.typeMeta.baz).to.be('qux'); }); + + it('can update runtime fields', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + expect(response1.body.index_pattern.title).to.be(title); + + expect(response1.body.index_pattern.runtimeFieldMap.runtimeFoo.type).to.be('keyword'); + expect(response1.body.index_pattern.runtimeFieldMap.runtimeFoo.script.source).to.be( + 'emit(doc["foo"].value)' + ); + + const id = response1.body.index_pattern.id; + const response2 = await supertest.post('/api/index_patterns/index_pattern/' + id).send({ + index_pattern: { + runtimeFieldMap: { + runtimeBar: { + type: 'keyword', + script: { + source: 'emit(doc["foo"].value)', + }, + }, + }, + }, + }); + + expect(response2.body.index_pattern.runtimeFieldMap.runtimeBar.type).to.be('keyword'); + expect(response2.body.index_pattern.runtimeFieldMap.runtimeFoo).to.be(undefined); + + const response3 = await supertest.get('/api/index_patterns/index_pattern/' + id); + + expect(response3.body.index_pattern.runtimeFieldMap.runtimeBar.type).to.be('keyword'); + expect(response3.body.index_pattern.runtimeFieldMap.runtimeFoo).to.be(undefined); + }); }); } diff --git a/test/api_integration/apis/index_patterns/integration/index.ts b/test/api_integration/apis/index_patterns/integration/index.ts new file mode 100644 index 0000000000000..6fd5f644ae894 --- /dev/null +++ b/test/api_integration/apis/index_patterns/integration/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +/** + * Test usage of different index patterns APIs in combination + */ +export default function ({ loadTestFile }: FtrProviderContext) { + describe('integration', () => { + loadTestFile(require.resolve('./integration')); + }); +} diff --git a/test/api_integration/apis/index_patterns/integration/integration.ts b/test/api_integration/apis/index_patterns/integration/integration.ts new file mode 100644 index 0000000000000..22f0755373323 --- /dev/null +++ b/test/api_integration/apis/index_patterns/integration/integration.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +/** + * Test usage of different index patterns APIs in combination + */ +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('integration', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('create an index pattern, add a runtime field, add a field formatter, then re-create the same index pattern', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + runtimeBar: { + count: 123, + customLabel: 'test', + }, + }, + }); + + expect(response3.status).to.be(200); + + const response4 = await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/fields`) + .send({ + fields: { + runtimeBar: { + format: { + id: 'duration', + params: { inputFormat: 'milliseconds', outputFormat: 'humanizePrecise' }, + }, + }, + }, + }); + + expect(response4.status).to.be(200); + + const response5 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response5.status).to.be(200); + + const resultIndexPattern = response5.body.index_pattern; + + const runtimeField = resultIndexPattern.fields.runtimeBar; + expect(runtimeField.name).to.be('runtimeBar'); + expect(runtimeField.runtimeField.type).to.be('long'); + expect(runtimeField.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(runtimeField.scripted).to.be(false); + + expect(resultIndexPattern.fieldFormats.runtimeBar.id).to.be('duration'); + expect(resultIndexPattern.fieldFormats.runtimeBar.params.inputFormat).to.be('milliseconds'); + expect(resultIndexPattern.fieldFormats.runtimeBar.params.outputFormat).to.be( + 'humanizePrecise' + ); + + expect(resultIndexPattern.fieldAttrs.runtimeBar.count).to.be(123); + expect(resultIndexPattern.fieldAttrs.runtimeBar.customLabel).to.be('test'); + + // check that retrieved object is transient and a clone can be created + const response6 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: resultIndexPattern, + }); + + expect(response6.status).to.be(200); + const recreatedIndexPattern = response6.body.index_pattern; + + expect(_.omit(recreatedIndexPattern, 'version')).to.eql( + _.omit(resultIndexPattern, 'version') + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/errors.ts new file mode 100644 index 0000000000000..8ce9e3b36b5c8 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/errors.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns an error field object is not provided', async () => { + const title = `foo-${Date.now()}-${Math.random()}*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({}); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be( + '[request body.name]: expected value of type [string] but got [undefined]' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/index.ts new file mode 100644 index 0000000000000..2cb90ca087f49 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('create_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts new file mode 100644 index 0000000000000..e262b9d838e97 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/create_runtime_field/main.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can create a new runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + const id = response1.body.index_pattern.id; + const response2 = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(200); + expect(response2.body.field.name).to.be('runtimeBar'); + expect(response2.body.field.runtimeField.type).to.be('long'); + expect(response2.body.field.runtimeField.script.source).to.be( + "emit(doc['field_name'].value)" + ); + expect(response2.body.field.scripted).to.be(false); + }); + + it('newly created runtime field is available in the index_pattern object', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + + await supertest + .post(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(response2.status).to.be(200); + + const field = response2.body.index_pattern.fields.runtimeBar; + + expect(field.name).to.be('runtimeBar'); + expect(field.runtimeField.type).to.be('long'); + expect(field.runtimeField.script.source).to.be("emit(doc['field_name'].value)"); + expect(field.scripted).to.be(false); + await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts new file mode 100644 index 0000000000000..b41a630889ff8 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/errors.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('errors', () => { + const basicIndex = 'b*sic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.delete( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing runtime field', async () => { + const response1 = await supertest.delete( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/test` + ); + + expect(response1.status).to.be(404); + }); + + it('returns error when attempting to delete a field which is not a runtime field', async () => { + const response2 = await supertest.delete( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be deleted.'); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.delete( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/index.ts new file mode 100644 index 0000000000000..a14201e750dda --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('delete_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/main.ts new file mode 100644 index 0000000000000..3c74aa336e440 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/delete_runtime_field/main.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can delete a runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeBar: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(typeof response2.body.index_pattern.fields.runtimeBar).to.be('object'); + + const response3 = await supertest.delete( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field/runtimeBar` + ); + + expect(response3.status).to.be(200); + + const response4 = await supertest.get( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + + expect(typeof response4.body.index_pattern.fields.runtimeBar).to.be('undefined'); + await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts new file mode 100644 index 0000000000000..3608089e4641a --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/errors.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('errors', () => { + const basicIndex = '*asic_index'; + let indexPattern: any; + + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + + indexPattern = ( + await supertest.post('/api/index_patterns/index_pattern').send({ + index_pattern: { + title: basicIndex, + }, + }) + ).body.index_pattern; + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + + if (indexPattern) { + await supertest.delete('/api/index_patterns/index_pattern/' + indexPattern.id); + } + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest.get( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(404); + }); + + it('returns 404 error on non-existing runtime field', async () => { + const response1 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/sf` + ); + + expect(response1.status).to.be(404); + }); + + it('returns error when ID is too long', async () => { + const id = `xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx`; + const response = await supertest.get( + `/api/index_patterns/index_pattern/${id}/runtime_field/foo` + ); + + expect(response.status).to.be(400); + expect(response.body.message).to.be( + '[request params.id]: value has length [1759] but it must have a maximum length of [1000].' + ); + }); + + it('returns error when attempting to fetch a field which is not a runtime field', async () => { + const response2 = await supertest.get( + `/api/index_patterns/index_pattern/${indexPattern.id}/runtime_field/foo` + ); + + expect(response2.status).to.be(400); + expect(response2.body.statusCode).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be retrieved.'); + }); + }); +} diff --git a/src/plugins/index_pattern_management/public/service/environment/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/index.ts similarity index 56% rename from src/plugins/index_pattern_management/public/service/environment/index.ts rename to test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/index.ts index ab5297ed0e14c..2e48ba64841ee 100644 --- a/src/plugins/index_pattern_management/public/service/environment/index.ts +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/index.ts @@ -6,4 +6,11 @@ * Side Public License, v 1. */ -export { EnvironmentService, Environment, EnvironmentServiceSetup } from './environment'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/main.ts new file mode 100644 index 0000000000000..fa0283d69d8e3 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/get_runtime_field/main.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can fetch a runtime field', async () => { + const title = `basic_index*`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }, + }, + }); + + expect(response1.status).to.be(200); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeFoo' + ); + + expect(response2.status).to.be(200); + expect(typeof response2.body.field).to.be('object'); + expect(response2.body.field.name).to.be('runtimeFoo'); + expect(response2.body.field.type).to.be('string'); + expect(response2.body.field.scripted).to.be(false); + expect(response2.body.field.runtimeField.script.source).to.be( + "emit(doc['field_name'].value)" + ); + await supertest.delete( + '/api/index_patterns/index_pattern/' + response1.body.index_pattern.id + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/index.ts new file mode 100644 index 0000000000000..7a727a3e86755 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('runtime_fields_crud', () => { + loadTestFile(require.resolve('./create_runtime_field')); + loadTestFile(require.resolve('./get_runtime_field')); + loadTestFile(require.resolve('./delete_runtime_field')); + loadTestFile(require.resolve('./put_runtime_field')); + loadTestFile(require.resolve('./update_runtime_field')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts new file mode 100644 index 0000000000000..9faca08238033 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/errors.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('errors', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .put(`/api/index_patterns/index_pattern/${id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error on non-runtime field update attempt', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + }, + }); + + const response2 = await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'bar', + runtimeField: { + type: 'long', + script: { + source: "emit(doc['field_name'].value)", + }, + }, + }); + + expect(response2.status).to.be(400); + expect(response2.body.message).to.be('Only runtime fields can be updated'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/index.ts new file mode 100644 index 0000000000000..724f18e57f3ef --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('put_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/main.ts new file mode 100644 index 0000000000000..92d8c6fd6d3c2 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/put_runtime_field/main.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can overwrite an existing field', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + const response2 = await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'runtimeFoo', + runtimeField: { + type: 'long', + script: { + source: "doc['field_name'].value", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeFoo' + ); + + expect(response3.status).to.be(200); + expect(response3.body.field.type).to.be('number'); + + const response4 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeBar' + ); + + expect(response4.status).to.be(200); + expect(response4.body.field.type).to.be('string'); + }); + + it('can add a new runtime field', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + await supertest + .put(`/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field`) + .send({ + name: 'runtimeBar', + runtimeField: { + type: 'long', + script: { + source: "doc['field_name'].value", + }, + }, + }); + + const response2 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeBar' + ); + + expect(response2.status).to.be(200); + expect(typeof response2.body.field.runtimeField).to.be('object'); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts new file mode 100644 index 0000000000000..3980821c0fd09 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/errors.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('errors', () => { + it('returns 404 error on non-existing index_pattern', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field/foo`) + .send({ + runtimeField: { + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response.status).to.be(404); + }); + + it('returns error when field name is specified', async () => { + const id = `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-${Date.now()}`; + const response = await supertest + .post(`/api/index_patterns/index_pattern/${id}/runtime_field/foo`) + .send({ + name: 'foo', + runtimeField: { + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response.status).to.be(400); + expect(response.body.statusCode).to.be(400); + expect(response.body.message).to.be( + "[request body.name]: a value wasn't expected to be present" + ); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/index.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/index.ts new file mode 100644 index 0000000000000..f5d556ca9994a --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('update_runtime_field', () => { + loadTestFile(require.resolve('./errors')); + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts new file mode 100644 index 0000000000000..6b924570a0e45 --- /dev/null +++ b/test/api_integration/apis/index_patterns/runtime_fields_crud/update_runtime_field/main.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('main', () => { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + + it('can update an existing field', async () => { + const title = `basic_index`; + const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, + index_pattern: { + title, + runtimeFieldMap: { + runtimeFoo: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + runtimeBar: { + type: 'keyword', + script: { + source: "doc['field_name'].value", + }, + }, + }, + }, + }); + + const response2 = await supertest + .post( + `/api/index_patterns/index_pattern/${response1.body.index_pattern.id}/runtime_field/runtimeFoo` + ) + .send({ + runtimeField: { + script: { + source: "doc['something_new'].value", + }, + }, + }); + + expect(response2.status).to.be(200); + + const response3 = await supertest.get( + '/api/index_patterns/index_pattern/' + + response1.body.index_pattern.id + + '/runtime_field/runtimeFoo' + ); + + expect(response3.status).to.be(200); + expect(response3.body.field.type).to.be('string'); + expect(response3.body.field.runtimeField.type).to.be('keyword'); + expect(response3.body.field.runtimeField.script.source).to.be("doc['something_new'].value"); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts index 1210e7247f72d..663deae1d3476 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/create_scripted_field/main.ts @@ -54,6 +54,7 @@ export default function ({ getService }: FtrProviderContext) { it('newly created scripted field is materialized in the index_pattern object', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, }, diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts index 466af26f6e127..b9ce3e84d5390 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/delete_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can remove a scripted field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts index 64d909480260e..c38c3a01708a7 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/get_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can fetch a scripted field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts index 96f5924e7c132..16b1958306818 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/put_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can overwrite an existing field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts index 055b3fe9abe04..4ffc98e0660bc 100644 --- a/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts +++ b/test/api_integration/apis/index_patterns/scripted_fields_crud/update_scripted_field/main.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { it('can update an existing field', async () => { const title = `basic_index`; const response1 = await supertest.post('/api/index_patterns/index_pattern').send({ + override: true, index_pattern: { title, fields: { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index c157765afb359..7f31aead1e83c 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -37,7 +37,7 @@ import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; import { UntypedNormalizedAlertType } from '../alert_type_registry'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; -import uuid from 'uuid'; + const alertType: jest.Mocked = { id: 'test', name: 'My test alert', @@ -373,6 +373,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -395,6 +397,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -525,6 +529,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -546,6 +552,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -669,7 +677,11 @@ describe('Task Runner', () => { meta: { lastScheduledActions: { date: '1970-01-01T00:00:00.000Z', group: 'default' }, }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 86400000000000, + }, }, }, }, @@ -699,6 +711,8 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -924,17 +938,221 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '1'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute-action", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "action_subgroup": undefined, + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + Object { + "id": "1", + "namespace": undefined, + "type": "action", + "type_id": "action", + }, + ], + }, + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + const runnerResult = await taskRunner.run(); + expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "lastScheduledActions": Object { + "date": 1970-01-01T00:00:00.000Z, + "group": "default", + "subgroup": undefined, + }, + }, + "state": Object { + "bar": false, + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", + }, + }, + } + `); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` + ); + expect(logger.debug).nthCalledWith( + 4, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { "event": Object { - "action": "new-instance", + "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { "alerting": Object { - "action_group_id": "default", - "instance_id": "1", + "instance_id": "2", }, "saved_objects": Array [ Object { @@ -946,13 +1164,15 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' created new instance: '1'", + "message": "test:1: 'alert-name' instance '2' has recovered", }, ], Array [ Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -972,6 +1192,36 @@ describe('Task Runner', () => { "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], + Array [ + Object { + "event": Object { + "action": "execute-action", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "recovered", + "action_subgroup": undefined, + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + Object { + "id": "2", + "namespace": undefined, + "type": "action", + "type_id": "action", + }, + ], + }, + "message": "alert: test:1: 'alert-name' instanceId: '2' scheduled actionGroup: 'recovered' action: action:2", + }, + ], Array [ Object { "event": Object { @@ -1028,84 +1278,7 @@ describe('Task Runner', () => { ], ] `); - }); - - test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { - taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); - taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); - - alertType.executor.mockImplementation( - async ({ - services: executorServices, - }: AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - string - >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - } - ); - const taskRunner = new TaskRunner( - alertType, - { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - alertInstances: { - '1': { meta: {}, state: { bar: false } }, - '2': { meta: {}, state: { bar: false } }, - }, - }, - }, - taskRunnerFactoryInitializerParams - ); - alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - }, - }, - } - `); - - const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); - expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); - expect(logger.debug).nthCalledWith( - 2, - `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` - ); - expect(logger.debug).nthCalledWith( - 3, - `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` - ); - expect(logger.debug).nthCalledWith( - 4, - 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' - ); - const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1129,7 +1302,7 @@ describe('Task Runner', () => { }); test('should skip alertInstances which werent active on the previous execution', async () => { - const alertId = uuid.v4(); + const alertId = 'e558aaad-fd81-46d2-96fc-3bd8fc3dc03f'; taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -1344,11 +1517,19 @@ describe('Task Runner', () => { alertInstances: { '1': { meta: { lastScheduledActions: { group: 'default', date } }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, }, '2': { meta: { lastScheduledActions: { group: 'default', date } }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, }, }, }, @@ -1377,6 +1558,8 @@ describe('Task Runner', () => { }, "state": Object { "bar": false, + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, }, } @@ -1390,6 +1573,9 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -1413,6 +1599,8 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -2040,4 +2228,705 @@ describe('Task Runner', () => { expect(isUnrecoverableError(ex)).toBeTruthy(); }); }); + + test('start time is logged for new alerts', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: {}, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '1'", + }, + ], + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '2'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('duration is updated for active alerts when alert state contains start time', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 64800000000000, + "start": "1969-12-31T06:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('duration is not calculated for active alerts when alert state does not contain start time', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { bar: false }, + }, + '2': { + meta: {}, + state: { bar: false }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('end is logged for active alerts when alert state contains start time and alert recovers', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation(async () => {}); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "recovered-instance", + "duration": 86400000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '1' has recovered", + }, + ], + Array [ + Object { + "event": Object { + "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '2' has recovered", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "ok", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('end calculation is skipped for active alerts when alert state does not contain start time and alert recovers', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => {} + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { bar: false }, + }, + '2': { + meta: {}, + state: { bar: false }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "recovered-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '1' has recovered", + }, + ], + Array [ + Object { + "event": Object { + "action": "recovered-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '2' has recovered", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "ok", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index fd82b38b493d7..f139617291a84 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -323,6 +323,12 @@ export class TaskRunner< alertLabel, }); + trackAlertDurations({ + originalAlerts: originalAlertInstances, + currentAlerts: instancesWithScheduledActions, + recoveredAlerts: recoveredAlertInstances, + }); + generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, @@ -589,6 +595,61 @@ export class TaskRunner< } } +interface TrackAlertDurationsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +> { + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; +} + +function trackAlertDurations< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +>(params: TrackAlertDurationsParams) { + const currentTime = new Date().toISOString(); + const { currentAlerts, originalAlerts, recoveredAlerts } = params; + const originalAlertIds = Object.keys(originalAlerts); + const currentAlertIds = Object.keys(currentAlerts); + const recoveredAlertIds = Object.keys(recoveredAlerts); + const newAlertIds = without(currentAlertIds, ...originalAlertIds); + + // Inject start time into instance state of new instances + for (const id of newAlertIds) { + const state = currentAlerts[id].getState(); + currentAlerts[id].replaceState({ ...state, start: currentTime }); + } + + // Calculate duration to date for active instances + for (const id of currentAlertIds) { + const state = originalAlertIds.includes(id) + ? originalAlerts[id].getState() + : currentAlerts[id].getState(); + const duration = state.start + ? (new Date(currentTime).valueOf() - new Date(state.start as string).valueOf()) * 1000 * 1000 // nanoseconds + : undefined; + currentAlerts[id].replaceState({ + ...state, + ...(state.start ? { start: state.start } : {}), + ...(duration !== undefined ? { duration } : {}), + }); + } + + // Inject end time into instance state of recovered instances + for (const id of recoveredAlertIds) { + const state = recoveredAlerts[id].getState(); + const duration = state.start + ? (new Date(currentTime).valueOf() - new Date(state.start as string).valueOf()) * 1000 * 1000 // nanoseconds + : undefined; + recoveredAlerts[id].replaceState({ + ...state, + ...(duration ? { duration } : {}), + ...(state.start ? { end: currentTime } : {}), + }); + } +} + interface GenerateNewAndRecoveredInstanceEventsParams< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext @@ -624,38 +685,66 @@ function generateNewAndRecoveredInstanceEvents< for (const id of recoveredAlertInstanceIds) { const { group: actionGroup, subgroup: actionSubgroup } = recoveredAlertInstances[id].getLastScheduledActions() ?? {}; + const state = recoveredAlertInstances[id].getState(); const message = `${params.alertLabel} instance '${id}' has recovered`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.recoveredInstance, + message, + state, + actionGroup, + actionSubgroup + ); } for (const id of newIds) { const { actionGroup, subgroup: actionSubgroup } = currentAlertInstances[id].getScheduledActionOptions() ?? {}; + const state = currentAlertInstances[id].getState(); const message = `${params.alertLabel} created new instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.newInstance, + message, + state, + actionGroup, + actionSubgroup + ); } for (const id of currentAlertInstanceIds) { const { actionGroup, subgroup: actionSubgroup } = currentAlertInstances[id].getScheduledActionOptions() ?? {}; + const state = currentAlertInstances[id].getState(); const message = `${params.alertLabel} active instance: '${id}' in ${ actionSubgroup ? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'` : `actionGroup: '${actionGroup}'` }`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.activeInstance, + message, + state, + actionGroup, + actionSubgroup + ); } function logInstanceEvent( instanceId: string, action: string, message: string, + state: InstanceState, group?: string, subgroup?: string ) { const event: IEvent = { event: { action, + ...(state?.start ? { start: state.start as string } : {}), + ...(state?.end ? { end: state.end as string } : {}), + ...(state?.duration !== undefined ? { duration: state.duration as number } : {}), }, kibana: { alerting: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 71355a84d28d4..d71751805ce41 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -5,14 +5,7 @@ * 2.0. */ -import { - EuiTitle, - EuiSpacer, - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { isString } from 'lodash'; @@ -100,17 +93,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { ); return ( - - -

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Indices

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

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

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

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

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

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

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

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

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

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

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

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

-
- - - {i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', { - defaultMessage: `Machine Learning's anomaly detection integration enables application health status indicators for services in each configured environment by identifying anomalies in latency.`, - })} - - {viewAddEnvironments ? ( environment)} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 9c69d692876b0..6df2ed73afe76 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -9,7 +9,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiSpacer, EuiText, EuiTitle, @@ -66,7 +65,28 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { const { jobs, hasLegacyJobs } = data; return ( - + <> + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText', + { + defaultMessage: 'Machine Learning', + } + )} + + ), + }} + /> + + + + @@ -91,25 +111,9 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - - - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText', - { - defaultMessage: 'Machine Learning', - } - )} - - ), - }} - /> - - + + + {hasLegacyJobs && } - + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx new file mode 100644 index 0000000000000..c6394f09b0d3c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { getRedirectToTransactionDetailPageUrl } from '../TraceLink/get_redirect_to_transaction_detail_page_url'; + +const CentralizedContainer = euiStyled.div` + height: 100%; + display: flex; +`; + +export function TransactionLink({ + match, +}: RouteComponentProps<{ transactionId: string }>) { + const { transactionId } = match.params; + const { urlParams } = useUrlParams(); + const { rangeFrom, rangeTo } = urlParams; + + const { data = { transaction: null }, status } = useFetcher( + (callApmApi) => { + if (transactionId) { + return callApmApi({ + endpoint: 'GET /api/apm/transactions/{transactionId}', + params: { + path: { + transactionId, + }, + }, + }); + } + }, + [transactionId] + ); + if (transactionId && status === FETCH_STATUS.SUCCESS) { + if (data.transaction) { + return ( + + ); + } + + return ; + } + + return ( + + Fetching transaction...} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index 09c25ee4557c5..36580d38e660d 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -20,6 +20,7 @@ import { AnomalyDetection } from '../app/Settings/anomaly_detection'; import { ApmIndices } from '../app/Settings/ApmIndices'; import { CustomizeUI } from '../app/Settings/CustomizeUI'; import { TraceLink } from '../app/TraceLink'; +import { TransactionLink } from '../app/transaction_link'; import { TransactionDetails } from '../app/transaction_details'; import { enableServiceOverview } from '../../../common/ui_settings_keys'; import { redirectTo } from './redirect_to'; @@ -219,41 +220,33 @@ function TransactionDetailsRouteView( function SettingsAgentConfigurationRouteView() { return ( - - - - - + + + ); } function SettingsAnomalyDetectionRouteView() { return ( - - - - - + + + ); } function SettingsApmIndicesRouteView() { return ( - - - - - + + + ); } function SettingsCustomizeUI() { return ( - - - - - + + + ); } @@ -276,14 +269,12 @@ export function EditAgentConfigurationRouteView(props: RouteComponentProps) { ); return ( - - - - - + + + ); } @@ -295,13 +286,11 @@ export function CreateAgentConfigurationRouteView(props: RouteComponentProps) { const { pageStep } = toQuery(search); return ( - - - - - + + + ); } @@ -510,6 +499,12 @@ export const apmRouteConfig: APMRouteDefinition[] = [ component: TraceLink, breadcrumb: null, }, + { + exact: true, + path: '/link-to/transaction/:transactionId', + component: TransactionLink, + breadcrumb: null, + }, ]; function RedirectToDefaultServiceRouteView( diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index e917350f6024b..357ba4c3d9f38 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -31,6 +31,7 @@ export function ApmMainTemplate({ children: React.ReactNode; } & EuiPageTemplateProps) { const { services } = useKibana(); + const ObservabilityPageTemplate = services.observability.navigation.PageTemplate; diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx index ab53052780ea9..bbaffbd3374c6 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template.tsx @@ -33,23 +33,21 @@ import { Correlations } from '../../app/correlations'; import { SearchBar } from '../../shared/search_bar'; type Tab = NonNullable[0] & { - key: string; + key: + | 'errors' + | 'metrics' + | 'nodes' + | 'overview' + | 'service-map' + | 'profiling' + | 'transactions'; hidden?: boolean; }; -type TabKey = - | 'errors' - | 'metrics' - | 'nodes' - | 'overview' - | 'service-map' - | 'profiling' - | 'transactions'; - interface Props { children: React.ReactNode; serviceName: string; - selectedTab: TabKey; + selectedTab: Tab['key']; searchBarOptions?: React.ComponentProps; } @@ -107,7 +105,7 @@ function useTabs({ selectedTab, }: { serviceName: string; - selectedTab: TabKey; + selectedTab: Tab['key']; }) { const { agentName, transactionType } = useApmServiceContext(); const { core, config } = useApmPluginContext(); diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx index 36d54525d2537..d3efef4c88380 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.test.tsx @@ -11,13 +11,28 @@ import React, { ReactNode } from 'react'; import { SettingsTemplate } from './settings_template'; import { createMemoryHistory } from 'history'; import { MemoryRouter, RouteComponentProps } from 'react-router-dom'; +import { CoreStart } from 'kibana/public'; +import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; const { location } = createMemoryHistory(); +const KibanaReactContext = createKibanaReactContext({ + usageCollection: { reportUiCounter: () => {} }, + observability: { + navigation: { + PageTemplate: () => { + return <>hello world; + }, + }, + }, +} as Partial); + function Wrapper({ children }: { children?: ReactNode }) { return ( - {children} + + {children} + ); } @@ -29,8 +44,8 @@ describe('Settings', () => { } as unknown) as RouteComponentProps<{}>; expect(() => render( - -
+ +
hello world
, { wrapper: Wrapper } ) diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx index 66958c01a7fea..0e610722a76e7 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.tsx @@ -5,90 +5,108 @@ * 2.0. */ -import { EuiPage, EuiPageBody, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; +import { EuiPageHeaderProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { ReactNode, useState } from 'react'; +import React from 'react'; +import { History } from 'history'; import { useHistory } from 'react-router-dom'; +import { CoreStart } from 'kibana/public'; +import { ApmMainTemplate } from './apm_main_template'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; -export function SettingsTemplate({ children }: { children: ReactNode }) { +type Tab = NonNullable[0] & { + key: + | 'agent-configurations' + | 'anomaly-detection' + | 'apm-indices' + | 'customize-ui'; + hidden?: boolean; +}; + +interface Props { + children: React.ReactNode; + selectedTab: Tab['key']; +} + +export function SettingsTemplate({ children, selectedTab }: Props) { const { core } = useApmPluginContext(); const history = useHistory(); - const { basePath } = core.http; - const canAccessML = !!core.application.capabilities.ml?.canAccessML; - const { search, pathname } = history.location; + const tabs = getTabs({ history, core, selectedTab }); - const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + return ( + + {children} + + ); +} - const toggleOpenOnMobile = () => { - setisSideNavOpenOnMobile((prevState) => !prevState); - }; +function getTabs({ + history, + core, + selectedTab, +}: { + history: History; + core: CoreStart; + selectedTab: Tab['key']; +}) { + const { basePath } = core.http; + const canAccessML = !!core.application.capabilities.ml?.canAccessML; + const { search } = history.location; - function getSettingsHref(path: string) { - return getAPMHref({ basePath, path: `/settings${path}`, search }); - } + const tabs: Tab[] = [ + { + key: 'agent-configurations', + label: i18n.translate('xpack.apm.settings.agentConfig', { + defaultMessage: 'Agent Configuration', + }), + href: getAPMHref({ + basePath, + path: `/settings/agent-configuration`, + search, + }), + }, + { + key: 'anomaly-detection', + label: i18n.translate('xpack.apm.settings.anomalyDetection', { + defaultMessage: 'Anomaly detection', + }), + href: getAPMHref({ + basePath, + path: `/settings/anomaly-detection`, + search, + }), + hidden: !canAccessML, + }, + { + key: 'customize-ui', + label: i18n.translate('xpack.apm.settings.customizeApp', { + defaultMessage: 'Customize app', + }), + href: getAPMHref({ basePath, path: `/settings/customize-ui`, search }), + }, + { + key: 'apm-indices', + label: i18n.translate('xpack.apm.settings.indices', { + defaultMessage: 'Indices', + }), + href: getAPMHref({ basePath, path: `/settings/apm-indices`, search }), + }, + ]; - return ( - - - toggleOpenOnMobile()} - isOpenOnMobile={isSideNavOpenOnMobile} - items={[ - { - name: i18n.translate('xpack.apm.settings.pageTitle', { - defaultMessage: 'Settings', - }), - id: 0, - items: [ - { - name: i18n.translate('xpack.apm.settings.agentConfig', { - defaultMessage: 'Agent Configuration', - }), - id: '1', - href: getSettingsHref('/agent-configuration'), - isSelected: pathname.startsWith( - '/settings/agent-configuration' - ), - }, - ...(canAccessML - ? [ - { - name: i18n.translate( - 'xpack.apm.settings.anomalyDetection', - { - defaultMessage: 'Anomaly detection', - } - ), - id: '4', - href: getSettingsHref('/anomaly-detection'), - isSelected: pathname === '/settings/anomaly-detection', - }, - ] - : []), - { - name: i18n.translate('xpack.apm.settings.customizeApp', { - defaultMessage: 'Customize app', - }), - id: '3', - href: getSettingsHref('/customize-ui'), - isSelected: pathname === '/settings/customize-ui', - }, - { - name: i18n.translate('xpack.apm.settings.indices', { - defaultMessage: 'Indices', - }), - id: '2', - href: getSettingsHref('/apm-indices'), - isSelected: pathname === '/settings/apm-indices', - }, - ], - }, - ]} - /> - - {children} - - ); + return tabs + .filter((t) => !t.hidden) + .map(({ href, key, label }) => ({ + href, + label, + isSelected: key === selectedTab, + })); } diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 24db9e0cd8504..bfc0a3daf6f0e 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -6,7 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { of } from 'rxjs'; +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; import type { ConfigSchema } from '.'; import { AppMountParameters, @@ -86,19 +87,56 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); } - // register observability nav + const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', { + defaultMessage: 'Services', + }); + const tracesTitle = i18n.translate('xpack.apm.navigation.tracesTitle', { + defaultMessage: 'Traces', + }); + const serviceMapTitle = i18n.translate( + 'xpack.apm.navigation.serviceMapTitle', + { defaultMessage: 'Service Map' } + ); + + // register observability nav if user has access to plugin plugins.observability.navigation.registerSections( - of([ - { - label: 'APM', - sortKey: 200, - entries: [ - { label: 'Services', app: 'apm', path: '/services' }, - { label: 'Traces', app: 'apm', path: '/traces' }, - { label: 'Service Map', app: 'apm', path: '/service-map' }, - ], - }, - ]) + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.apm.show) { + return [ + // APM navigation + { + label: 'APM', + sortKey: 200, + entries: [ + { label: servicesTitle, app: 'apm', path: '/services' }, + { label: tracesTitle, app: 'apm', path: '/traces' }, + { label: serviceMapTitle, app: 'apm', path: '/service-map' }, + ], + }, + + // UX navigation + { + label: 'User Experience', + sortKey: 201, + entries: [ + { + label: i18n.translate('xpack.apm.ux.overview.heading', { + defaultMessage: 'Overview', + }), + app: 'ux', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + ], + }, + ]; + } + + return []; + }) + ) ); const getApmDataHelper = async () => { @@ -150,26 +188,6 @@ export class ApmPlugin implements Plugin { }, }); - plugins.observability.navigation.registerSections( - of([ - { - label: 'User Experience', - sortKey: 201, - entries: [ - { - label: i18n.translate('xpack.apm.ux.overview.heading', { - defaultMessage: 'Overview', - }), - app: 'ux', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - ], - }, - ]) - ); - core.application.register({ id: 'apm', title: 'APM', @@ -178,29 +196,10 @@ export class ApmPlugin implements Plugin { appRoute: '/app/apm', icon: 'plugins/apm/public/icon.svg', category: DEFAULT_APP_CATEGORIES.observability, - // !! Need to be kept in sync with the routes in x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx deepLinks: [ - { - id: 'services', - title: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { - defaultMessage: 'Services', - }), - path: '/services', - }, - { - id: 'traces', - title: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { - defaultMessage: 'Traces', - }), - path: '/traces', - }, - { - id: 'service-map', - title: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map', - }), - path: '/service-map', - }, + { id: 'services', title: servicesTitle, path: '/services' }, + { id: 'traces', title: tracesTitle, path: '/traces' }, + { id: 'service-map', title: serviceMapTitle, path: '/service-map' }, ], async mount(appMountParameters: AppMountParameters) { diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index b4323ae7f51e2..6987ef0757734 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -21,11 +21,11 @@ export function getTransaction({ setup, }: { transactionId: string; - traceId: string; - setup: Setup & SetupTimeRange; + traceId?: string; + setup: Setup | (Setup & SetupTimeRange); }) { return withApmSpan('get_transaction', async () => { - const { start, end, apmEventClient } = setup; + const { apmEventClient } = setup; const resp = await apmEventClient.search({ apm: { @@ -37,8 +37,8 @@ export function getTransaction({ bool: { filter: asMutableArray([ { term: { [TRANSACTION_ID]: transactionId } }, - { term: { [TRACE_ID]: traceId } }, - ...rangeQuery(start, end), + ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []), + ...('start' in setup ? rangeQuery(setup.start, setup.end) : []), ]), }, }, diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index dd392982b02fd..7fce04644f220 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -14,6 +14,7 @@ import { environmentRt, kueryRt, rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { getTransaction } from '../lib/transactions/get_transaction'; const tracesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/traces', @@ -70,7 +71,24 @@ const rootTransactionByTraceIdRoute = createApmServerRoute({ }, }); +const transactionByIdRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/transactions/{transactionId}', + params: t.type({ + path: t.type({ + transactionId: t.string, + }), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const { params } = resources; + const { transactionId } = params.path; + const setup = await setupRequest(resources); + return { transaction: await getTransaction({ transactionId, setup }) }; + }, +}); + export const traceRouteRepository = createApmServerRouteRepository() .add(tracesByIdRoute) .add(tracesRoute) - .add(rootTransactionByTraceIdRoute); + .add(rootTransactionByTraceIdRoute) + .add(transactionByIdRoute); diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index 3934f0ee3417f..b024a52e64721 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -19,6 +19,7 @@ "lens" ], "requiredBundles": [ + "home", "kibanaReact", "maps", "esUiShared" diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index 20d2e93fd6879..66109de1b1463 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -19,7 +19,7 @@ import type { SecurityPluginSetup } from '../../security/public'; import type { LensPublicStart } from '../../lens/public'; import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api'; import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; -import { registerHomeAddData } from './register_home'; +import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; export interface DataVisualizerSetupDependencies { home?: HomePublicPluginSetup; @@ -48,6 +48,7 @@ export class DataVisualizerPlugin public setup(core: CoreSetup, plugins: DataVisualizerSetupDependencies) { if (plugins.home) { registerHomeAddData(plugins.home); + registerHomeFeatureCatalogue(plugins.home); } } diff --git a/x-pack/plugins/data_visualizer/public/register_home.ts b/x-pack/plugins/data_visualizer/public/register_home.ts index 0b438dc309c4b..3e8973784433c 100644 --- a/x-pack/plugins/data_visualizer/public/register_home.ts +++ b/x-pack/plugins/data_visualizer/public/register_home.ts @@ -7,14 +7,34 @@ import { i18n } from '@kbn/i18n'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { FileDataVisualizerWrapper } from './lazy_load_bundle/component_wrapper'; +const FILE_DATA_VIS_TAB_ID = 'fileDataViz'; + export function registerHomeAddData(home: HomePublicPluginSetup) { home.addData.registerAddDataTab({ - id: 'fileDataViz', + id: FILE_DATA_VIS_TAB_ID, name: i18n.translate('xpack.dataVisualizer.file.embeddedTabTitle', { defaultMessage: 'Upload file', }), component: FileDataVisualizerWrapper, }); } + +export function registerHomeFeatureCatalogue(home: HomePublicPluginSetup) { + home.featureCatalogue.register({ + id: `file_data_visualizer`, + title: i18n.translate('xpack.dataVisualizer.title', { + defaultMessage: 'Upload a file', + }), + description: i18n.translate('xpack.dataVisualizer.description', { + defaultMessage: 'Import your own CSV, NDJSON, or log file.', + }), + icon: 'document', + path: `/app/home#/tutorial_directory/${FILE_DATA_VIS_TAB_ID}`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + order: 520, + }); +} diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 753100f622556..a9393abcc57ef 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -61,9 +61,9 @@ export interface FullAgentPolicyInput { } export interface FullAgentPolicyOutputPermissions { - [role: string]: { - cluster: string[]; - indices: Array<{ + [packagePolicyName: string]: { + cluster?: string[]; + indices?: Array<{ names: string[]; privileges: string[]; }>; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5551453b8975c..0ef9f8b7ace36 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -276,6 +276,7 @@ export enum RegistryDataStreamKeys { ingest_pipeline = 'ingest_pipeline', elasticsearch = 'elasticsearch', dataset_is_prefix = 'dataset_is_prefix', + permissions = 'permissions', } export interface RegistryDataStream { @@ -291,6 +292,7 @@ export interface RegistryDataStream { [RegistryDataStreamKeys.ingest_pipeline]?: string; [RegistryDataStreamKeys.elasticsearch]?: RegistryElasticsearch; [RegistryDataStreamKeys.dataset_is_prefix]?: boolean; + [RegistryDataStreamKeys.permissions]?: RegistryDataStreamPermissions; } export interface RegistryElasticsearch { @@ -298,6 +300,11 @@ export interface RegistryElasticsearch { 'index_template.mappings'?: object; } +export interface RegistryDataStreamPermissions { + cluster?: string[]; + indices?: string[]; +} + export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml' | 'string'; export enum RegistryVarsEntryKeys { name = 'name', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 4063bc6371cbd..3477335321a2c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -109,9 +109,7 @@ describe('when on integration detail', () => { pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' })[1] ); }); - expect(testRenderer.history.location.pathname).toEqual( - '/app/integrations/detail/nginx-0.3.7/overview' - ); + expect(testRenderer.history.location.pathname).toEqual('/detail/nginx-0.3.7/overview'); }); }); diff --git a/x-pack/plugins/fleet/public/hooks/use_link.ts b/x-pack/plugins/fleet/public/hooks/use_link.ts index df6f45af31d56..6917e0f5c3b8e 100644 --- a/x-pack/plugins/fleet/public/hooks/use_link.ts +++ b/x-pack/plugins/fleet/public/hooks/use_link.ts @@ -21,9 +21,7 @@ export const useLink = () => { const core = useStartServices(); return { getPath: (page: StaticPage | DynamicPage, values: DynamicPagePathValues = {}): string => { - const [basePath, path] = getSeparatePaths(page, values); - - return `${basePath}${path}`; + return getSeparatePaths(page, values)[1]; }, getAssetsPath: (path: string) => core.http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 62b4578ab87b2..2a6036d99281e 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -47,6 +47,10 @@ import type { Output, } from '../../common'; import { AgentPolicyNameExistsError, HostedAgentPolicyRestrictionRelatedError } from '../errors'; +import { + storedPackagePoliciesToAgentPermissions, + DEFAULT_PERMISSIONS, +} from '../services/package_policies_to_agent_permissions'; import { getPackageInfo } from './epm/packages'; import { getAgentsByKuery } from './agents'; @@ -745,30 +749,49 @@ class AgentPolicyService { }), }; + const permissions = (await storedPackagePoliciesToAgentPermissions( + soClient, + agentPolicy.package_policies + )) || { _fallback: DEFAULT_PERMISSIONS }; + + permissions._elastic_agent_checks = { + cluster: DEFAULT_PERMISSIONS.cluster, + }; + + // TODO fetch this from the elastic agent package + const monitoringOutput = fullAgentPolicy.agent?.monitoring.use_output; + const monitoringNamespace = fullAgentPolicy.agent?.monitoring.namespace; + if ( + fullAgentPolicy.agent?.monitoring.enabled && + monitoringNamespace && + monitoringOutput && + fullAgentPolicy.outputs[monitoringOutput]?.type === 'elasticsearch' + ) { + const names: string[] = []; + if (fullAgentPolicy.agent.monitoring.logs) { + names.push(`logs-elastic_agent.*-${monitoringNamespace}`); + } + if (fullAgentPolicy.agent.monitoring.metrics) { + names.push(`metrics-elastic_agent.*-${monitoringNamespace}`); + } + + permissions._elastic_agent_checks.indices = [ + { + names, + privileges: ['auto_configure', 'create_doc'], + }, + ]; + } + // Only add permissions if output.type is "elasticsearch" fullAgentPolicy.output_permissions = Object.keys(fullAgentPolicy.outputs).reduce< NonNullable - >((permissions, outputName) => { + >((outputPermissions, outputName) => { const output = fullAgentPolicy.outputs[outputName]; if (output && output.type === 'elasticsearch') { - permissions[outputName] = {}; - permissions[outputName]._fallback = { - cluster: ['monitor'], - indices: [ - { - names: [ - 'logs-*', - 'metrics-*', - 'traces-*', - '.logs-endpoint.diagnostic.collection-*', - 'synthetics-*', - ], - privileges: ['auto_configure', 'create_doc'], - }, - ], - }; + outputPermissions[outputName] = permissions; } - return permissions; + return outputPermissions; }, {}); // only add settings if not in standalone diff --git a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts index 2db6009270a3b..dde6459addcbc 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/storage.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/storage.ts @@ -224,24 +224,20 @@ export const getEsPackage = async ( ); const dataStreamManifest = safeLoad(soResDataStreamManifest.attributes.data_utf8); const { - title: dataStreamTitle, - release, ingest_pipeline: ingestPipeline, - type, dataset, streams: manifestStreams, + ...dataStreamManifestProps } = dataStreamManifest; const streams = parseAndVerifyStreams(manifestStreams, dataStreamPath); dataStreams.push({ dataset: dataset || `${pkgName}.${dataStreamPath}`, - title: dataStreamTitle, - release, package: pkgName, ingest_pipeline: ingestPipeline || 'default', path: dataStreamPath, - type, streams, + ...dataStreamManifestProps, }); }) ); diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts new file mode 100644 index 0000000000000..39759a6fc9e9c --- /dev/null +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -0,0 +1,341 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('./epm/packages'); +import type { SavedObjectsClientContract } from 'kibana/server'; + +import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import type { PackagePolicy, RegistryDataStream } from '../types'; + +import { getPackageInfo } from './epm/packages'; +import { + getDataStreamPermissions, + storedPackagePoliciesToAgentPermissions, +} from './package_policies_to_agent_permissions'; + +const getPackageInfoMock = getPackageInfo as jest.MockedFunction; + +describe('storedPackagePoliciesToAgentPermissions()', () => { + let soClient: jest.Mocked; + beforeEach(() => { + soClient = savedObjectsClientMock.create(); + }); + + it('Returns `undefined` if there are no package policies', async () => { + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, []); + expect(permissions).toBeUndefined(); + }); + + it('Returns the default permissions for string package policies', async () => { + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, ['foo']); + expect(permissions).toMatchObject({ + _fallback: { + cluster: ['monitor'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + 'synthetics-*', + '.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); + + it('Returns the default permissions if a package policy does not have a package', async () => { + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, [ + { name: 'foo', package: undefined } as PackagePolicy, + ]); + + expect(permissions).toMatchObject({ + foo: { + cluster: ['monitor'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + 'synthetics-*', + '.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); + + it('Returns the permissions for the enabled inputs', async () => { + getPackageInfoMock.mockResolvedValueOnce({ + name: 'test-package', + version: '0.0.0', + latestVersion: '0.0.0', + release: 'experimental', + format_version: '1.0.0', + title: 'Test Package', + description: '', + icons: [], + owner: { github: '' }, + status: 'not_installed', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + }, + }, + data_streams: [ + { + type: 'logs', + dataset: 'some-logs', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-logs', title: 'Test Logs', template_path: '' }], + }, + { + type: 'metrics', + dataset: 'some-metrics', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-metrics', title: 'Test Logs', template_path: '' }], + }, + ], + }); + + const packagePolicies: PackagePolicy[] = [ + { + id: '12345', + name: 'test-policy', + namespace: 'test', + enabled: true, + package: { name: 'test-package', version: '0.0.0', title: 'Test Package' }, + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + }, + ], + }, + { + type: 'test-metrics', + enabled: false, + streams: [ + { + id: 'test-logs', + enabled: false, + data_stream: { type: 'metrics', dataset: 'some-metrics' }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + output_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); + expect(permissions).toMatchObject({ + 'test-policy': { + indices: [ + { + names: ['logs-some-logs-test'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); + + it('Returns the dataset for the compiled data_streams', async () => { + getPackageInfoMock.mockResolvedValueOnce({ + name: 'test-package', + version: '0.0.0', + latestVersion: '0.0.0', + release: 'experimental', + format_version: '1.0.0', + title: 'Test Package', + description: '', + icons: [], + owner: { github: '' }, + status: 'not_installed', + assets: { + kibana: { + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + }, + }, + data_streams: [ + { + type: 'logs', + dataset: 'some-logs', + title: '', + release: '', + package: 'test-package', + path: '', + ingest_pipeline: '', + streams: [{ input: 'test-logs', title: 'Test Logs', template_path: '' }], + }, + ], + }); + + const packagePolicies: PackagePolicy[] = [ + { + id: '12345', + name: 'test-policy', + namespace: 'test', + enabled: true, + package: { name: 'test-package', version: '0.0.0', title: 'Test Package' }, + inputs: [ + { + type: 'test-logs', + enabled: true, + streams: [ + { + id: 'test-logs', + enabled: true, + data_stream: { type: 'logs', dataset: 'some-logs' }, + compiled_stream: { data_stream: { dataset: 'compiled' } }, + }, + ], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + output_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions(soClient, packagePolicies); + expect(permissions).toMatchObject({ + 'test-policy': { + indices: [ + { + names: ['logs-compiled-test'], + privileges: ['auto_configure', 'create_doc'], + }, + ], + }, + }); + }); +}); + +describe('getDataStreamPermissions()', () => { + it('returns defaults for a datastream with no permissions', () => { + const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream); + + expect(permissions).toMatchObject({ + names: ['logs-test-*'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('adds the namespace to the index name', () => { + const dataStream = { type: 'logs', dataset: 'test' } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['logs-test-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('appends a wildcard if dataset is prefix', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + dataset_is_prefix: true, + } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['logs-test.*-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('prepends a dot if datastream is hidden', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + hidden: true, + } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['.logs-test-namespace'], + privileges: ['auto_configure', 'create_doc'], + }); + }); + + it('uses custom permissions if they are present in the datastream', () => { + const dataStream = { + type: 'logs', + dataset: 'test', + permissions: { indices: ['read', 'write'] }, + } as RegistryDataStream; + const permissions = getDataStreamPermissions(dataStream, 'namespace'); + + expect(permissions).toMatchObject({ + names: ['logs-test-namespace'], + privileges: ['read', 'write'], + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts new file mode 100644 index 0000000000000..bd73b88e7c893 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { SavedObjectsClientContract } from 'kibana/server'; + +import type { FullAgentPolicyOutputPermissions, RegistryDataStreamPermissions } from '../../common'; +import { getPackageInfo } from '../../server/services/epm/packages'; + +import type { PackagePolicy } from '../types'; + +export const DEFAULT_PERMISSIONS = { + cluster: ['monitor'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + 'synthetics-*', + '.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['auto_configure', 'create_doc'], + }, + ], +}; + +export async function storedPackagePoliciesToAgentPermissions( + soClient: SavedObjectsClientContract, + packagePolicies: string[] | PackagePolicy[] +): Promise { + if (packagePolicies.length === 0) { + return; + } + + // I'm not sure what permissions to return for this case, so let's return the defaults + if (typeof packagePolicies[0] === 'string') { + return { _fallback: DEFAULT_PERMISSIONS }; + } + + const permissionEntries = (packagePolicies as PackagePolicy[]).map>( + async (packagePolicy) => { + if (!packagePolicy.package) { + return [packagePolicy.name, DEFAULT_PERMISSIONS]; + } + + const pkg = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + }); + + if (!pkg.data_streams || pkg.data_streams.length === 0) { + return [packagePolicy.name, undefined]; + } + + let dataStreamsForPermissions: DataStreamMeta[]; + + switch (pkg.name) { + case 'endpoint': + // - Endpoint doesn't store the `data_stream` metadata in + // `packagePolicy.inputs`, so we will use _all_ data_streams from the + // package. + dataStreamsForPermissions = pkg.data_streams; + break; + + case 'apm': + // - APM doesn't store the `data_stream` metadata in + // `packagePolicy.inputs`, so we will use _all_ data_streams from + // the package. + dataStreamsForPermissions = pkg.data_streams; + break; + + default: + // - Normal packages store some of the `data_stream` metadata in + // `packagePolicy.inputs[].streams[].data_stream` + // - The rest of the metadata needs to be fetched from the + // `data_stream` object in the package. The link is + // `packagePolicy.inputs[].type == pkg.data_streams.streams[].input` + // - Some packages (custom logs) have a compiled dataset, stored in + // `input.streams.compiled_stream.data_stream.dataset` + dataStreamsForPermissions = packagePolicy.inputs + .filter((i) => i.enabled) + .flatMap((input) => { + if (!input.streams) { + return []; + } + + const dataStreams_: DataStreamMeta[] = []; + + input.streams + .filter((s) => s.enabled) + .forEach((stream) => { + if (!('data_stream' in stream)) { + return; + } + + const ds = { + type: stream.data_stream.type, + dataset: + stream.compiled_stream?.data_stream?.dataset ?? stream.data_stream.dataset, + }; + + dataStreams_.push(ds); + }); + + return dataStreams_; + }); + } + + return [ + packagePolicy.name, + { + indices: dataStreamsForPermissions.map((ds) => + getDataStreamPermissions(ds, packagePolicy.namespace) + ), + }, + ]; + } + ); + + return Object.fromEntries(await Promise.all(permissionEntries)); +} + +interface DataStreamMeta { + type: string; + dataset: string; + dataset_is_prefix?: boolean; + hidden?: boolean; + permissions?: RegistryDataStreamPermissions; +} + +export function getDataStreamPermissions(dataStream: DataStreamMeta, namespace: string = '*') { + let index = `${dataStream.type}-${dataStream.dataset}`; + + if (dataStream.dataset_is_prefix) { + index = `${index}.*`; + } + + if (dataStream.hidden) { + index = `.${index}`; + } + + index += `-${namespace}`; + + return { + names: [index], + privileges: dataStream.permissions?.indices || ['auto_configure', 'create_doc'], + }; +} diff --git a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html index 4e0c13442d267..b2363ffbaa641 100644 --- a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html +++ b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html @@ -9,4 +9,5 @@ initial-filter="initialFilter" initialPageSize="initialPageSize" core-start="coreStart" + class="kbnAppWrapper" > diff --git a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index 6c2c79baafe0a..7fa63404a4abd 100644 --- a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -86,8 +86,8 @@ function GuidancePanelComponent(props: GuidancePanelProps) { const kibana = useKibana(); const { services, overlays } = kibana; - const { savedObjects, uiSettings, chrome, application } = services; - if (!overlays || !chrome || !application) return null; + const { savedObjects, uiSettings, application } = services; + if (!overlays || !application) return null; const onOpenDatasourcePicker = () => { openSourceModal({ overlays, savedObjects, uiSettings }, onIndexPatternSelected); @@ -149,8 +149,9 @@ function GuidancePanelComponent(props: GuidancePanelProps) { ); if (noIndexPatterns) { - const managementUrl = chrome.navLinks.get('kibana:stack_management')!.url; - const indexPatternUrl = `${managementUrl}/kibana/indexPatterns`; + const indexPatternUrl = application.getUrlForApp('management', { + path: '/kibana/indexPatterns', + }); const sampleDataUrl = `${application.getUrlForApp('home')}#/tutorial_directory/sampleData`; content = ( diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx index 77b0372e24994..b99d828b8bbd0 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.helpers.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { createBreadcrumbsMock } from '../../../public/application/services/breadcrumbs.mock'; import { licensingMock } from '../../../../licensing/public/mocks'; import { App } from '../../../public/application/app'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts index 36f04be3b30b1..18af96dd2804b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/app/app.test.ts @@ -5,15 +5,17 @@ * 2.0. */ +import { act } from 'react-dom/test-utils'; + +import { getDefaultHotPhasePolicy, POLICY_NAME } from '../edit_policy/constants'; +import { setupEnvironment } from '../helpers'; + import { AppTestBed, getDoubleEncodedPolicyEditPath, getEncodedPolicyEditPath, setup, } from './app.helpers'; -import { setupEnvironment } from '../helpers/setup_environment'; -import { getDefaultHotPhasePolicy, POLICY_NAME } from '../edit_policy/constants'; -import { act } from 'react-dom/test-utils'; const SPECIAL_CHARS_NAME = 'test?#$+=&@:'; const PERCENT_SIGN_NAME = 'test%'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 58da73942000d..6f1c58b2e9b18 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -8,8 +8,9 @@ import { TestBedConfig } from '@kbn/test/jest'; import { AppServicesContext } from '../../../public/types'; +import { Phase } from '../../../common/types'; + import { - Phase, createNodeAllocationActions, createFormToggleAction, createFormSetValueAction, @@ -27,7 +28,6 @@ import { createReadonlyActions, createIndexPriorityActions, } from '../helpers'; - import { initTestBed } from './init_test_bed'; type SetupReturn = ReturnType; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts index 6403cfead8bd0..0c24101461f24 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts @@ -5,16 +5,16 @@ * 2.0. */ +import { act } from 'react-dom/test-utils'; +import { API_BASE_PATH } from '../../../../common/constants'; +import { setupEnvironment } from '../../helpers'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; import { DELETE_PHASE_POLICY, getDefaultHotPhasePolicy, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME, } from '../constants'; -import { act } from 'react-dom/test-utils'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { API_BASE_PATH } from '../../../../common/constants'; -import { setupEnvironment } from '../../helpers/setup_environment'; describe(' delete phase', () => { let testBed: EditPolicyTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts index 3736e9c43e269..982377e2a0365 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' frozen phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts index 7ca68bd76759e..136865329eb50 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { CloudNodeAllocationTestBed, setupCloudNodeAllocation, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts index 3b968c1dca4ab..e2093e04b7779 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/cold_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { NodeAllocationTestBed, setupColdPhaseNodeAllocation } from './cold_phase.helpers'; describe(' node allocation in the cold phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts index d45e1cfe9567b..cac6c174b769e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/general_behavior.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { GeneralNodeAllocationTestBed, setupGeneralNodeAllocation, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts index 1a1ed988d266e..8d4d0afcf052d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation/warm_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../../helpers/setup_environment'; +import { setupEnvironment } from '../../../helpers'; import { NodeAllocationTestBed, setupWarmPhaseNodeAllocation } from './warm_phase.helpers'; describe(' node allocation in the warm phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts index de9a1b403ad33..1cb895e9ac86a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts @@ -6,8 +6,8 @@ */ import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { setupEnvironment } from '../../helpers/setup_environment'; describe(' request flyout', () => { let testBed: EditPolicyTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts index b58fe4086c7f0..8e9586e52577b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' rollover', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts index 21a76b16c17ef..d400966cdae38 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy } from '../constants'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts index 5a117caa99378..a4f2a24bcee8b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' timeline', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts index 8ec2f2e1b4598..ae9f306483820 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' warm phase', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts index 669bcfdd34f63..9a14571c6ec3b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' cold phase validation', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts index 5c70a9025df67..0a047714bd345 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts @@ -6,7 +6,7 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' error indicators', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts index 879aab872b19b..296b128eb8f52 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' hot phase validation', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts index 41d2b6d166dce..08b794466da49 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/policy_name_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; import { getGeneratedPolicies } from '../constants'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts index c164605e5cd4b..ac11e8a162e02 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts @@ -8,8 +8,9 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; +import { PhaseWithTiming } from '../../../../common/types'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { setupEnvironment } from '../../helpers/setup_environment'; describe(' timing validation', () => { let testBed: EditPolicyTestBed; @@ -69,12 +70,12 @@ describe(' timing validation', () => { ['warm', 'cold', 'delete', 'frozen'].forEach((phase: string) => { const { name, value, error } = testConfig; test(`${phase}: ${name}`, async () => { - await actions.togglePhase(phase as 'warm' | 'cold' | 'delete' | 'frozen'); + await actions.togglePhase(phase as PhaseWithTiming); // 1. We first set as dummy value to have a starting min_age value - await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue('111'); + await actions[phase as PhaseWithTiming].setMinAgeValue('111'); // 2. At this point we are sure there will be a change of value and that any validation // will be displayed under the field. - await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue(value); + await actions[phase as PhaseWithTiming].setMinAgeValue(value); actions.errors.waitForValidation(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts index 70794491d4edd..bef99ea8cb891 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { setupEnvironment } from '../../helpers'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; describe(' warm phase validation', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index dfc81e9e0d9d7..8c345cf784f9f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -6,14 +6,14 @@ */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; +import { licensingMock } from '../../../../../licensing/public/mocks'; +import { setupEnvironment } from '../../helpers'; import { getDefaultHotPhasePolicy, POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, } from '../constants'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { licensingMock } from '../../../../../licensing/public/mocks'; describe(' serialization', () => { let testBed: EditPolicyTestBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts index 8d5708950a75f..a92747a95a2ca 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/errors_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; const createWaitForValidationAction = (testBed: TestBed) => () => { const { component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts index 61b16d18648f5..a7e4983165bac 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/forcemerge_actions.ts @@ -7,9 +7,9 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; const createFormCheckboxAction = (testBed: TestBed, dataTestSubject: string) => async ( checked: boolean diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts index 87b03ff4e1deb..ad3d9d3bfbcb8 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/freeze_actions.ts @@ -6,8 +6,8 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; -import { Phase } from '../types'; export const createFreezeActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts index a0423c12be5c3..3b48da2a0c69f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/index_priority_actions.ts @@ -6,9 +6,9 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; export const createIndexPriorityActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts index 4a2491f3561ee..551474c8b16e6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/min_age_actions.ts @@ -6,8 +6,8 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; export const createMinAgeActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts index b4b97b7c88a5e..7c0f8fea7299d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/node_allocation_actions.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; import { DataTierAllocationType } from '../../../../public/application/sections/edit_policy/types'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; import { createFormSetValueAction } from './form_set_value_action'; export const createNodeAllocationActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts index ee9161c91209e..b76143eccf1f2 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/readonly_actions.ts @@ -6,8 +6,8 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; -import { Phase } from '../types'; export const createReadonlyActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts index d7ccd32694759..f1a64c3943511 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/searchable_snapshot_actions.ts @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; import { TestBed } from '@kbn/test/jest'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; export const createSearchableSnapshotActions = (testBed: TestBed, phase: Phase) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts index cabd3012001b7..b07d7783379fb 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/set_replicas_action.ts @@ -7,7 +7,7 @@ import { TestBed } from '@kbn/test/jest'; -import { Phase } from '../types'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts index 9d227ef2624a4..0531850384124 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/shrink_actions.ts @@ -6,9 +6,9 @@ */ import { TestBed } from '@kbn/test/jest'; +import { Phase } from '../../../../common/types'; import { createFormToggleAction } from './form_toggle_action'; import { createFormSetValueAction } from './form_set_value_action'; -import { Phase } from '../types'; export const createShrinkActions = (testBed: TestBed, phase: Phase) => { const { exists } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts index fb1575d05bdef..a0bed0e1644e6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/actions/toggle_phase_action.ts @@ -6,9 +6,9 @@ */ import { TestBed } from '@kbn/test/jest'; - import { act } from 'react-dom/test-utils'; -import { Phase } from '../types'; + +import { Phase } from '../../../../common/types'; const toggleDeletePhase = async (testBed: TestBed) => { const { find, component } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 5b2e1827cd6e8..f9cd1f27b72be 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { Phase } from './types'; - export * from './actions'; + +export { setupEnvironment } from './setup_environment'; diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 385751f31d5bc..b08f87e5a172b 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -7,8 +7,14 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; +export type Phase = keyof Phases; + export type PhaseWithAllocation = 'warm' | 'cold'; +export type PhaseWithTiming = keyof Omit; + +export type PhaseExceptDelete = keyof Omit; + export interface SerializedPolicy { name: string; phases: Phases; @@ -22,8 +28,6 @@ export interface Phases { delete?: SerializedDeletePhase; } -export type PhasesExceptDelete = keyof Omit; - export interface PolicyFromES { modified_date: string; name: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx index 22422ceab8a04..0428c2939050d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiText, EuiButtonGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { PhasesExceptDelete } from '../../../../../../common/types'; +import { PhaseExceptDelete } from '../../../../../../common/types'; import { usePhaseTimings } from '../../form'; @@ -32,7 +32,7 @@ const keepDataLabel = i18n.translate( ); interface Props { - phase: PhasesExceptDelete; + phase: PhaseExceptDelete; } export const PhaseFooter: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx index 040baf3625eb8..f5ab67c0b3ee7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx @@ -20,7 +20,7 @@ import { import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PhasesExceptDelete } from '../../../../../../../common/types'; +import { PhaseExceptDelete } from '../../../../../../../common/types'; import { ToggleField, useFormData } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; import { FormInternal } from '../../../types'; @@ -33,7 +33,7 @@ import { PhaseErrorIndicator } from './phase_error_indicator'; import './phase.scss'; interface Props { - phase: PhasesExceptDelete; + phase: PhaseExceptDelete; /** * Settings that should always be visible on the phase when it is enabled. */ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx index 47d2aa6ba92df..0f5288df36119 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx @@ -10,15 +10,16 @@ import React, { FunctionComponent, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiTextColor } from '@elastic/eui'; -import { NumericField } from '../../../../../../shared_imports'; +import { PhaseExceptDelete } from '../../../../../../../common/types'; +import { NumericField } from '../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../edit_policy_context'; -import { UseField } from '../../../form'; +import { UseField } from '../../../form'; import { LearnMoreLink, DescribedFormRow } from '../..'; interface Props { - phase: 'hot' | 'warm' | 'cold' | 'frozen'; + phase: PhaseExceptDelete; } export const IndexPriorityField: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx index 136a37140cca7..ad92ef2d44479 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -21,13 +21,12 @@ import { EuiIconTip, } from '@elastic/eui'; +import { PhaseWithTiming } from '../../../../../../../../common/types'; import { getFieldValidityAndErrorMessage, useFormData } from '../../../../../../../shared_imports'; import { UseField, useConfiguration, useGlobalFields } from '../../../../form'; import { getPhaseMinAgeInMilliseconds } from '../../../../lib'; import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; -type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; - const i18nTexts = { daysOptionLabel: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.daysOptionLabel', { defaultMessage: 'days', @@ -77,7 +76,7 @@ const i18nTexts = { }; interface Props { - phase: PhaseWithMinAgeAction; + phase: PhaseWithTiming; } export const MinAgeField: FunctionComponent = ({ phase }): React.ReactElement => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts index a626a0a62f551..5f1b98c6dbda2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/util.ts @@ -6,12 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import { Phases } from '../../../../../../../../common/types'; +import { PhaseWithTiming } from '../../../../../../../../common/types'; -type PhaseWithMinAgeAction = 'warm' | 'cold' | 'delete'; - -export function getUnitsAriaLabelForPhase(phase: keyof Phases) { - // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. +export function getUnitsAriaLabelForPhase(phase: PhaseWithTiming) { switch (phase) { case 'warm': return i18n.translate( @@ -29,6 +26,14 @@ export function getUnitsAriaLabelForPhase(phase: keyof Phases) { } ); + case 'frozen': + return i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeUnitsAriaLabel', + { + defaultMessage: 'Units for timing of frozen phase', + } + ); + case 'delete': return i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel', @@ -38,8 +43,7 @@ export function getUnitsAriaLabelForPhase(phase: keyof Phases) { ); } } -export function getTimingLabelForPhase(phase: PhaseWithMinAgeAction) { - // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. +export function getTimingLabelForPhase(phase: PhaseWithTiming) { switch (phase) { case 'warm': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', { @@ -51,6 +55,11 @@ export function getTimingLabelForPhase(phase: PhaseWithMinAgeAction) { defaultMessage: 'Timing for cold phase', }); + case 'frozen': + return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseFrozen.minimumAgeLabel', { + defaultMessage: 'Timing for frozen phase', + }); + case 'delete': return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', { defaultMessage: 'Timing for delete phase', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 6edc6568ef766..de5c258ab82fa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, memo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiIconTip } from '@elastic/eui'; -import { PhasesExceptDelete } from '../../../../../../common/types'; +import { PhaseExceptDelete } from '../../../../../../common/types'; import { calculateRelativeFromAbsoluteMilliseconds, @@ -136,7 +136,7 @@ export const Timeline: FunctionComponent = memo( const widths = calculateWidths(phaseAgeInMilliseconds); - const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => + const getDurationInPhaseContent = (phase: PhaseExceptDelete): string | React.ReactNode => phaseAgeInMilliseconds.phases[phase] === Infinity ? ( ) : null; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index f24d350432d80..c26f54cbb6f5a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; +import { PhaseExceptDelete, PhaseWithTiming } from '../../../../../common/types'; import { FormSchema, fieldValidators } from '../../../../shared_imports'; import { defaultIndexPriority } from '../../../constants'; import { ROLLOVER_FORM_PATHS, CLOUD_DEFAULT_REPO } from '../constants'; -import { MinAgePhase } from '../types'; import { i18nTexts } from '../i18n_texts'; import { ifExistsNumberGreaterThanZero, @@ -107,8 +107,8 @@ const numberOfShardsField = { serializer: serializers.stringToNumber, }; -const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ - defaultValue: defaultIndexPriority[phase] as any, +const getPriorityField = (phase: PhaseExceptDelete) => ({ + defaultValue: defaultIndexPriority[phase], label: i18nTexts.editPolicy.indexPriorityFieldLabel, validations: [ { @@ -119,7 +119,7 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ serializer: serializers.stringToNumber, }); -const getMinAgeField = (phase: MinAgePhase, defaultValue?: string) => ({ +const getMinAgeField = (phase: PhaseWithTiming, defaultValue?: string) => ({ defaultValue, // By passing an empty array we make sure to *not* trigger the validation when the field value changes. // The validation will be triggered when the millisecond variant (in the _meta) is updated (in sync) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts index b39b3f3cd4226..892e40f80f4b8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts @@ -16,8 +16,8 @@ import { import { ROLLOVER_FORM_PATHS } from '../constants'; import { i18nTexts } from '../i18n_texts'; -import { PolicyFromES } from '../../../../../common/types'; -import { FormInternal, MinAgePhase } from '../types'; +import { PhaseWithTiming, PolicyFromES } from '../../../../../common/types'; +import { FormInternal } from '../types'; const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators; @@ -164,7 +164,7 @@ export const createPolicyNameValidations = ({ * For example, the user can't define '5 days' for cold phase if the * warm phase is set to '10 days'. */ -export const minAgeGreaterThanPreviousPhase = (phase: MinAgePhase) => ({ +export const minAgeGreaterThanPreviousPhase = (phase: PhaseWithTiming) => ({ formData, }: { formData: Record; @@ -173,7 +173,7 @@ export const minAgeGreaterThanPreviousPhase = (phase: MinAgePhase) => ({ return; } - const getValueFor = (_phase: MinAgePhase) => { + const getValueFor = (_phase: PhaseWithTiming) => { const milli = formData[`_meta.${_phase}.minAgeToMilliSeconds`]; const esFormat = diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 9d55f542db4c4..71e2741c27cdc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -22,17 +22,16 @@ import moment from 'moment'; -import { splitSizeAndUnits } from '../../../lib/policies'; +import { Phase, PhaseWithTiming } from '../../../../../common/types'; -import { FormInternal, MinAgePhase } from '../types'; +import { splitSizeAndUnits } from '../../../lib/policies'; +import { FormInternal } from '../types'; /* -===- Private functions and types -===- */ -type Phase = 'hot' | MinAgePhase; - const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'frozen', 'delete']; -const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ +const getMinAge = (phase: PhaseWithTiming, formData: FormInternal) => ({ min_age: formData.phases?.[phase]?.min_age ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit : '0ms', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 987cbbcfde45b..ba7d31cf6da49 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -18,8 +18,6 @@ export interface MinAgeField { minAgeToMilliSeconds: number; } -export type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete'; - export interface ForcemergeFields { bestCompression: boolean; } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx index 88948e9a7615b..49003af28f3f1 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiButtonGroup } from '@elastic/eui'; +import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui'; import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types'; import { DatatableVisualizationState } from '../visualization'; import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks'; @@ -212,4 +212,64 @@ describe('data table dimension editor', () => { expect(instance.find(PalettePanelContainer).exists()).toBe(true); }); + + it('should show the summary field for non numeric columns', () => { + const instance = mountWithIntl(); + expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_function"]').exists()).toBe( + false + ); + expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false); + }); + + it('should set the summary row function default to "none"', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_function"]') + .find(EuiComboBox) + .prop('selectedOptions') + ).toEqual([{ value: 'none', label: 'None' }]); + + expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false); + }); + + it('should show the summary row label input ony when summary row is different from "none"', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns[0].summaryRow = 'sum'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_function"]') + .find(EuiComboBox) + .prop('selectedOptions') + ).toEqual([{ value: 'sum', label: 'Sum' }]); + + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_label"]') + .find(EuiFieldText) + .prop('value') + ).toBe('Sum'); + }); + + it("should show the correct summary row name when user's changes summary label", () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + state.columns[0].summaryRow = 'sum'; + state.columns[0].summaryLabel = 'MySum'; + const instance = mountWithIntl(); + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_function"]') + .find(EuiComboBox) + .prop('selectedOptions') + ).toEqual([{ value: 'sum', label: 'Sum' }]); + + expect( + instance + .find('[data-test-subj="lnsDatatable_summaryrow_label"]') + .find(EuiFieldText) + .prop('value') + ).toBe('MySum'); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx index 76c47a9c743c5..6c39a04ae1504 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, @@ -16,10 +16,12 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonEmpty, + EuiFieldText, + EuiComboBox, } from '@elastic/eui'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { VisualizationDimensionEditorProps } from '../../types'; -import { DatatableVisualizationState } from '../visualization'; +import { ColumnState, DatatableVisualizationState } from '../visualization'; import { getOriginalId } from '../transpose_helpers'; import { CustomizablePalette, @@ -27,14 +29,22 @@ import { defaultPaletteParams, FIXED_PROGRESSION, getStopsForFixedMode, + useDebouncedValue, } from '../../shared_components/'; import { PalettePanelContainer } from './palette_panel_container'; import { findMinMaxByColumnId } from './shared_utils'; import './dimension_editor.scss'; +import { + getDefaultSummaryLabel, + getFinalSummaryConfiguration, + getSummaryRowOptions, +} from '../summary'; +import { isNumericField } from '../utils'; const idPrefix = htmlIdGenerator()(); type ColumnType = DatatableVisualizationState['columns'][number]; +type SummaryRowType = Extract; function updateColumnWith( state: DatatableVisualizationState, @@ -58,6 +68,24 @@ export function TableDimensionEditor( const { state, setState, frame, accessor } = props; const column = state.columns.find(({ columnId }) => accessor === columnId); const [isPaletteOpen, setIsPaletteOpen] = useState(false); + const onSummaryLabelChangeToDebounce = useCallback( + (newSummaryLabel: string | undefined) => { + setState({ + ...state, + columns: updateColumnWith(state, accessor, { summaryLabel: newSummaryLabel }), + }); + }, + [accessor, setState, state] + ); + const { inputValue: summaryLabel, handleInputChange: onSummaryLabelChange } = useDebouncedValue< + string | undefined + >( + { + onChange: onSummaryLabelChangeToDebounce, + value: column?.summaryLabel, + }, + { allowEmptyString: true } // empty string is a valid label for this feature + ); if (!column) return null; if (column.isTransposed) return null; @@ -65,13 +93,16 @@ export function TableDimensionEditor( const currentData = frame.activeData?.[state.layerId]; // either read config state or use same logic as chart itself - const isNumericField = - currentData?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) - ?.meta.type === 'number'; - - const currentAlignment = column?.alignment || (isNumericField ? 'right' : 'left'); + const isNumeric = isNumericField(currentData, accessor); + const currentAlignment = column?.alignment || (isNumeric ? 'right' : 'left'); const currentColorMode = column?.colorMode || 'none'; const hasDynamicColoring = currentColorMode !== 'none'; + // when switching from one operation to another, make sure to keep the configuration consistent + const { summaryRow, summaryLabel: fallbackSummaryLabel } = getFinalSummaryConfiguration( + accessor, + column, + currentData + ); const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length; @@ -175,7 +206,61 @@ export function TableDimensionEditor( /> )} - {isNumericField && ( + {isNumeric && ( + <> + + { + const newValue = choices[0].value as SummaryRowType; + setState({ + ...state, + columns: updateColumnWith(state, accessor, { summaryRow: newValue }), + }); + }} + /> + + {summaryRow !== 'none' && ( + + { + onSummaryLabelChange(e.target.value); + }} + /> + + )} + + )} + {isNumeric && ( <> { c: { min: 3, max: 3 }, }); }); + + test('it does render a summary footer if at least one column has it configured', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-a"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').first().text()).toEqual( + 'Sum: 3' + ); + }); + + test('it does render a summary footer with just the raw value for empty label', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').first().text()).toEqual('3'); + }); + + test('it does not render the summary row if the only column with summary is hidden', () => { + const { data, args } = sampleArgs(); + + const wrapper = mountWithIntl( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + paletteService={chartPluginMock.createPaletteRegistry()} + uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient} + /> + ); + + expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index e6fcf3f321f7f..cd990149fdaf5 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -43,6 +43,7 @@ import { } from './table_actions'; import { findMinMaxByColumnId } from './shared_utils'; import { CUSTOM_PALETTE } from '../../shared_components/coloring/constants'; +import { getFinalSummaryConfiguration } from '../summary'; export const DataContext = React.createContext({}); @@ -286,6 +287,40 @@ export const DatatableComponent = (props: DatatableRenderProps) => { [onEditAction, sortBy, sortDirection] ); + const renderSummaryRow = useMemo(() => { + const columnsWithSummary = columnConfig.columns + .filter((col) => !!col.columnId && !col.hidden) + .map((config) => ({ + columnId: config.columnId, + summaryRowValue: config.summaryRowValue, + ...getFinalSummaryConfiguration(config.columnId, config, firstTable), + })) + .filter(({ summaryRow }) => summaryRow !== 'none'); + + if (columnsWithSummary.length) { + const summaryLookup = Object.fromEntries( + columnsWithSummary.map(({ summaryRowValue, summaryLabel, columnId }) => [ + columnId, + summaryLabel === '' ? `${summaryRowValue}` : `${summaryLabel}: ${summaryRowValue}`, + ]) + ); + return ({ columnId }: { columnId: string }) => { + const currentAlignment = alignments && alignments[columnId]; + const alignmentClassName = `lnsTableCell--${currentAlignment}`; + const columnName = + columns.find(({ id }) => id === columnId)?.displayAsText?.replace(/ /g, '-') || columnId; + return summaryLookup[columnId] != null ? ( +
+ {summaryLookup[columnId]} +
+ ) : null; + }; + } + }, [columnConfig.columns, alignments, firstTable, columns]); + if (isEmpty) { return ; } @@ -323,6 +358,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { sorting={sorting} onColumnResize={onColumnResize} toolbarVisibility={false} + renderFooterCellValue={renderSummaryRow} /> diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 2d5f4aea98856..79a541b0288ab 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -28,10 +28,12 @@ import { ColumnState } from './visualization'; import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } from '../types'; import type { DatatableRender } from './components/types'; import { transposeTable } from './transpose_helpers'; +import { computeSummaryRowForColumn } from './summary'; export type ColumnConfigArg = Omit & { type: 'lens_datatable_column'; palette?: PaletteOutput; + summaryRowValue?: unknown; }; export interface Args { @@ -116,6 +118,16 @@ export const getDatatable = ({ return memo; }, {}); + const columnsWithSummary = args.columns.filter((c) => c.summaryRow); + for (const column of columnsWithSummary) { + column.summaryRowValue = computeSummaryRowForColumn( + column, + firstTable, + formatters, + formatFactory({ id: 'number' }) + ); + } + if (sortBy && columnsReverseLookup[sortBy] && sortDirection !== 'none') { // Sort on raw values for these types, while use the formatted value for the rest const sortingCriteria = getSortingCriteria( @@ -173,6 +185,8 @@ export const datatableColumn: ExpressionFunctionDefinition< types: ['palette'], help: '', }, + summaryRow: { types: ['string'], help: '' }, + summaryLabel: { types: ['string'], help: '' }, }, fn: function fn(input: unknown, args: ColumnState) { return { diff --git a/x-pack/plugins/lens/public/datatable_visualization/summary.test.ts b/x-pack/plugins/lens/public/datatable_visualization/summary.test.ts new file mode 100644 index 0000000000000..f92c83fbbfdc8 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/summary.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IFieldFormat } from 'src/plugins/data/public'; +import { Datatable } from 'src/plugins/expressions'; +import { computeSummaryRowForColumn, getFinalSummaryConfiguration } from './summary'; + +describe('Summary row helpers', () => { + const mockNumericTable: Datatable = { + type: 'datatable', + columns: [{ id: 'myColumn', name: 'My Column', meta: { type: 'number' } }], + rows: [{ myColumn: 45 }], + }; + + const mockNumericTableWithArray: Datatable = { + type: 'datatable', + columns: [{ id: 'myColumn', name: 'My Column', meta: { type: 'number' } }], + rows: [{ myColumn: [45, 90] }], + }; + + const mockNonNumericTable: Datatable = { + type: 'datatable', + columns: [{ id: 'myColumn', name: 'My Column', meta: { type: 'string' } }], + rows: [{ myColumn: 'myString' }], + }; + + const defaultFormatter = { convert: (x) => x } as IFieldFormat; + const customNumericFormatter = { convert: (x: number) => x.toFixed(2) } as IFieldFormat; + + describe('getFinalSummaryConfiguration', () => { + it('should return the base configuration for an unconfigured column', () => { + expect(getFinalSummaryConfiguration('myColumn', {}, mockNumericTable)).toEqual({ + summaryRow: 'none', + summaryLabel: 'None', + }); + }); + + it('should return the right configuration for a partially configured column', () => { + expect( + getFinalSummaryConfiguration('myColumn', { summaryRow: 'sum' }, mockNumericTable) + ).toEqual({ + summaryRow: 'sum', + summaryLabel: 'Sum', + }); + }); + + it('should return the base configuration for a transitioned invalid column', () => { + expect( + getFinalSummaryConfiguration('myColumn', { summaryRow: 'sum' }, mockNumericTableWithArray) + ).toEqual({ + summaryRow: 'sum', + summaryLabel: 'Sum', + }); + }); + + it('should return the base configuration for a non numeric column', () => { + expect( + getFinalSummaryConfiguration('myColumn', { summaryRow: 'sum' }, mockNonNumericTable) + ).toEqual({ + summaryRow: 'none', + summaryLabel: 'None', + }); + }); + }); + + describe('computeSummaryRowForColumn', () => { + for (const op of ['avg', 'sum', 'min', 'max'] as const) { + it(`should return formatted value for a ${op} summary function`, () => { + expect( + computeSummaryRowForColumn( + { summaryRow: op, columnId: 'myColumn', type: 'lens_datatable_column' }, + mockNumericTable, + { + myColumn: customNumericFormatter, + }, + defaultFormatter + ) + ).toBe('45.00'); + }); + } + + it('should ignore the column formatter, rather return the raw value for count operation', () => { + expect( + computeSummaryRowForColumn( + { summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' }, + mockNumericTable, + { + myColumn: customNumericFormatter, + }, + defaultFormatter + ) + ).toBe(1); + }); + + it('should only count non-null/empty values', () => { + expect( + computeSummaryRowForColumn( + { summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' }, + { ...mockNumericTable, rows: [...mockNumericTable.rows, { myColumn: null }] }, + { + myColumn: customNumericFormatter, + }, + defaultFormatter + ) + ).toBe(1); + }); + + it('should count numeric arrays as valid and distinct values', () => { + expect( + computeSummaryRowForColumn( + { summaryRow: 'count', columnId: 'myColumn', type: 'lens_datatable_column' }, + mockNumericTableWithArray, + { + myColumn: defaultFormatter, + }, + defaultFormatter + ) + ).toBe(2); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/summary.ts b/x-pack/plugins/lens/public/datatable_visualization/summary.ts new file mode 100644 index 0000000000000..6c267445aab76 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/summary.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { FieldFormat } from 'src/plugins/data/public'; +import { Datatable } from 'src/plugins/expressions/public'; +import { ColumnConfigArg } from './datatable_visualization'; +import { getOriginalId } from './transpose_helpers'; +import { isNumericField } from './utils'; + +type SummaryRowType = Extract; + +export function getFinalSummaryConfiguration( + columnId: string, + columnArgs: Pick | undefined, + table: Datatable | undefined +) { + const isNumeric = isNumericField(table, columnId); + + const summaryRow = isNumeric ? columnArgs?.summaryRow || 'none' : 'none'; + const summaryLabel = columnArgs?.summaryLabel ?? getDefaultSummaryLabel(summaryRow); + + return { + summaryRow, + summaryLabel, + }; +} + +export function getDefaultSummaryLabel(type: SummaryRowType) { + return getSummaryRowOptions().find(({ value }) => type === value)!.label!; +} + +export function getSummaryRowOptions(): Array<{ + value: SummaryRowType; + label: string; + 'data-test-subj': string; +}> { + return [ + { + value: 'none', + label: i18n.translate('xpack.lens.table.summaryRow.none', { + defaultMessage: 'None', + }), + 'data-test-subj': 'lns-datatable-summary-none', + }, + { + value: 'count', + label: i18n.translate('xpack.lens.table.summaryRow.count', { + defaultMessage: 'Value count', + }), + 'data-test-subj': 'lns-datatable-summary-count', + }, + { + value: 'sum', + label: i18n.translate('xpack.lens.table.summaryRow.sum', { + defaultMessage: 'Sum', + }), + 'data-test-subj': 'lns-datatable-summary-sum', + }, + { + value: 'avg', + label: i18n.translate('xpack.lens.table.summaryRow.average', { + defaultMessage: 'Average', + }), + 'data-test-subj': 'lns-datatable-summary-avg', + }, + { + value: 'min', + label: i18n.translate('xpack.lens.table.summaryRow.minimum', { + defaultMessage: 'Minimum', + }), + 'data-test-subj': 'lns-datatable-summary-min', + }, + { + value: 'max', + label: i18n.translate('xpack.lens.table.summaryRow.maximum', { + defaultMessage: 'Maximum', + }), + 'data-test-subj': 'lns-datatable-summary-max', + }, + ]; +} + +export function computeSummaryRowForColumn( + columnArgs: ColumnConfigArg, + table: Datatable, + formatters: Record, + defaultFormatter: FieldFormat +) { + const summaryValue = computeFinalValue(columnArgs.summaryRow, columnArgs.columnId, table.rows); + // ignore the coluymn formatter for the count case + if (columnArgs.summaryRow === 'count') { + return defaultFormatter.convert(summaryValue); + } + return formatters[getOriginalId(columnArgs.columnId)].convert(summaryValue); +} + +function computeFinalValue( + type: ColumnConfigArg['summaryRow'], + columnId: string, + rows: Datatable['rows'] +) { + // flatten the row structure, to easier handle numeric arrays + const validRows = rows.filter((v) => v[columnId] != null).flatMap((v) => v[columnId]); + const count = validRows.length; + const sum = validRows.reduce((partialSum: number, value: number) => { + return partialSum + value; + }, 0); + switch (type) { + case 'sum': + return sum; + case 'count': + return count; + case 'avg': + return sum / count; + case 'min': + return Math.min(...validRows); + case 'max': + return Math.max(...validRows); + default: + throw Error('No summary function found'); + } +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/utils.ts b/x-pack/plugins/lens/public/datatable_visualization/utils.ts new file mode 100644 index 0000000000000..64fdee233e830 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/utils.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable } from 'src/plugins/expressions/public'; +import { getOriginalId } from './transpose_helpers'; + +function isValidNumber(value: unknown): boolean { + return typeof value === 'number' || value == null; +} + +export function isNumericField(currentData: Datatable | undefined, accessor: string) { + const isNumeric = + currentData?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor) + ?.meta.type === 'number'; + + return ( + isNumeric && + currentData?.rows.every((row) => { + const val = row[accessor]; + return isValidNumber(val) || (Array.isArray(val) && val.every(isValidNumber)); + }) + ); +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index efde4160019e7..e48cb1b28c084 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -23,6 +23,7 @@ import { TableDimensionEditor } from './components/dimension_editor'; import { CUSTOM_PALETTE } from '../shared_components/coloring/constants'; import { CustomPaletteParams } from '../shared_components/coloring/types'; import { getStopsForFixedMode } from '../shared_components'; +import { getDefaultSummaryLabel } from './summary'; export interface ColumnState { columnId: string; @@ -38,6 +39,8 @@ export interface ColumnState { alignment?: 'left' | 'right' | 'center'; palette?: PaletteOutput; colorMode?: 'none' | 'cell' | 'text'; + summaryRow?: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max'; + summaryLabel?: string; } export interface SortingState { @@ -358,6 +361,8 @@ export const getDatatableVisualization = ({ reverse: false, // managed at UI level }; + const hasNoSummaryRow = column.summaryRow == null || column.summaryRow === 'none'; + return { type: 'expression', chain: [ @@ -376,6 +381,10 @@ export const getDatatableVisualization = ({ alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], colorMode: [column.colorMode ?? 'none'], palette: [paletteService.get(CUSTOM_PALETTE).toExpression(paletteParams)], + summaryRow: hasNoSummaryRow ? [] : [column.summaryRow!], + summaryLabel: hasNoSummaryRow + ? [] + : [column.summaryLabel ?? getDefaultSummaryLabel(column.summaryRow!)], }, }, ], diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.test.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.test.ts new file mode 100644 index 0000000000000..7aa93fcad95e9 --- /dev/null +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { useDebouncedValue } from './debounced_value'; + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +describe('useDebouncedValue', () => { + it('should update upstream value changes', () => { + const onChangeMock = jest.fn(); + const { result } = renderHook(() => useDebouncedValue({ value: 'a', onChange: onChangeMock })); + + act(() => { + result.current.handleInputChange('b'); + }); + + expect(onChangeMock).toHaveBeenCalledWith('b'); + }); + + it('should fallback to initial value with empty string (by default)', () => { + const onChangeMock = jest.fn(); + const { result } = renderHook(() => useDebouncedValue({ value: 'a', onChange: onChangeMock })); + + act(() => { + result.current.handleInputChange(''); + }); + + expect(onChangeMock).toHaveBeenCalledWith('a'); + }); + + it('should allow empty input to be updated', () => { + const onChangeMock = jest.fn(); + const { result } = renderHook(() => + useDebouncedValue({ value: 'a', onChange: onChangeMock }, { allowEmptyString: true }) + ); + + act(() => { + result.current.handleInputChange(''); + }); + + expect(onChangeMock).toHaveBeenCalledWith(''); + }); +}); diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts index 1f8ba0fa765b2..5525f6b16b316 100644 --- a/x-pack/plugins/lens/public/shared_components/debounced_value.ts +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts @@ -13,15 +13,19 @@ import { debounce } from 'lodash'; * are in flight because the user is currently modifying the value. */ -export const useDebouncedValue = ({ - onChange, - value, -}: { - onChange: (val: T) => void; - value: T; -}) => { +export const useDebouncedValue = ( + { + onChange, + value, + }: { + onChange: (val: T) => void; + value: T; + }, + { allowEmptyString }: { allowEmptyString?: boolean } = {} +) => { const [inputValue, setInputValue] = useState(value); const unflushedChanges = useRef(false); + const shouldUpdateWithEmptyString = Boolean(allowEmptyString); // Save the initial value const initialValue = useRef(value); @@ -45,7 +49,10 @@ export const useDebouncedValue = ({ const handleInputChange = (val: T) => { setInputValue(val); - onChangeDebounced(val || initialValue.current); + const valueToUpload = shouldUpdateWithEmptyString + ? val ?? initialValue.current + : val || initialValue.current; + onChangeDebounced(valueToUpload); }; return { inputValue, handleInputChange, initialValue: initialValue.current }; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 92bac61f3fab3..f34172765e1dd 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,6 @@ "embeddable", "uiActions", "kibanaLegacy", - "indexPatternManagement", "discover", "triggersActionsUi" ], diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx index 8375b0a0b1dfc..d20042ddd9443 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx @@ -8,20 +8,21 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; import { isEqual } from 'lodash'; import { - EuiPanel, - EuiPopover, - EuiContextMenuPanel, + EuiButtonEmpty, EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, EuiFlexGroup, EuiFlexItem, + EuiPanel, + EuiPopover, EuiSelect, - EuiTitle, EuiSpacer, - EuiContextMenuItem, - EuiButtonEmpty, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import useDebounce from 'react-use/lib/useDebounce'; import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants'; import { AddSwimlaneToDashboardControl } from './dashboard_controls/add_swimlane_to_dashboard_controls'; import { useMlKibana } from '../contexts/kibana'; @@ -93,6 +94,18 @@ export const AnomalyTimeline: FC = React.memo( viewBySwimlaneData, } = explorerState; + const [severityUpdate, setSeverityUpdate] = useState(swimLaneSeverity); + + useDebounce( + () => { + if (severityUpdate === swimLaneSeverity) return; + + explorerService.setSwimLaneSeverity(severityUpdate!); + }, + 500, + [severityUpdate, swimLaneSeverity] + ); + const annotations = useMemo(() => overallAnnotations.annotationsData, [overallAnnotations]); const menuItems = useMemo(() => { @@ -194,9 +207,9 @@ export const AnomalyTimeline: FC = React.memo( { - explorerService.setSwimLaneSeverity(update); + setSeverityUpdate(update); }, [])} /> diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 42440883408ae..1191f3b253fd7 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -24,14 +24,12 @@ import type { } from 'src/plugins/share/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; -import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import type { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import type { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; import type { LicenseManagementUIPluginSetup } from '../../license_management/public'; import type { LicensingPluginSetup } from '../../licensing/public'; @@ -78,7 +76,6 @@ export interface MlSetupDependencies { uiActions: UiActionsSetup; kibanaVersion: string; share: SharePluginSetup; - indexPatternManagement: IndexPatternManagementSetup; triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup; alerting?: AlertingSetup; } @@ -148,12 +145,6 @@ export class MlPlugin implements Plugin { if (pluginsSetup.home) { registerFeature(pluginsSetup.home); } - - // register ML for the index pattern management no data screen. - pluginsSetup.indexPatternManagement.environment.update({ - ml: () => - capabilities.ml.canFindFileStructure ? MlCardState.ENABLED : MlCardState.HIDDEN, - }); } else { // if ml is disabled in elasticsearch, disable ML in kibana this.appUpdater$.next(() => ({ diff --git a/x-pack/plugins/ml/public/register_feature.ts b/x-pack/plugins/ml/public/register_feature.ts index 191e064c85b90..e8d64360df123 100644 --- a/x-pack/plugins/ml/public/register_feature.ts +++ b/x-pack/plugins/ml/public/register_feature.ts @@ -13,10 +13,6 @@ import { import { PLUGIN_ID } from '../common/constants/app'; export const registerFeature = (home: HomePublicPluginSetup) => { - // register ML for the kibana home screen. - // so the file data visualizer appears to allow people to import data - home.environment.update({ ml: true }); - // register ML so it appears on the Kibana home page home.featureCatalogue.register({ id: PLUGIN_ID, @@ -37,19 +33,4 @@ export const registerFeature = (home: HomePublicPluginSetup) => { solutionId: 'kibana', order: 500, }); - - home.featureCatalogue.register({ - id: `${PLUGIN_ID}_file_data_visualizer`, - title: i18n.translate('xpack.ml.fileDataVisualizerTitle', { - defaultMessage: 'Upload a file', - }), - description: i18n.translate('xpack.ml.fileDataVisualizerDescription', { - defaultMessage: 'Import your own CSV, NDJSON, or log file.', - }), - icon: 'document', - path: '/app/ml/filedatavisualizer', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - order: 520, - }); }; diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index 8b99ac3c5761b..034c25758a1f0 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -6,3 +6,4 @@ */ export { SecurityLicense } from './licensing'; +export { AuthenticatedUser } from './model'; diff --git a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts index ba3297aeb5493..ba73d3b196d2f 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.mock.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.mock.ts @@ -8,10 +8,10 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { apiKeysMock } from './api_keys/api_keys.mock'; -import type { AuthenticationServiceStart } from './authentication_service'; +import type { AuthenticationServiceStartInternal } from './authentication_service'; export const authenticationServiceMock = { - createStart: (): DeeplyMockedKeys => ({ + createStart: (): DeeplyMockedKeys => ({ apiKeys: apiKeysMock.create(), login: jest.fn(), logout: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index e5895422e7a74..946fedbeee04f 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -51,7 +51,7 @@ interface AuthenticationServiceStartParams { loggers: LoggerFactory; } -export interface AuthenticationServiceStart { +export interface AuthenticationServiceStartInternal extends AuthenticationServiceStart { apiKeys: Pick< APIKeys, | 'areAPIKeysEnabled' @@ -66,6 +66,21 @@ export interface AuthenticationServiceStart { getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; } +/** + * Authentication services available on the security plugin's start contract. + */ +export interface AuthenticationServiceStart { + apiKeys: Pick< + APIKeys, + | 'areAPIKeysEnabled' + | 'create' + | 'invalidate' + | 'grantAsInternalUser' + | 'invalidateAsInternalUser' + >; + getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; +} + export class AuthenticationService { private license!: SecurityLicense; private authenticator?: Authenticator; @@ -212,7 +227,7 @@ export class AuthenticationService { legacyAuditLogger, loggers, session, - }: AuthenticationServiceStartParams): AuthenticationServiceStart { + }: AuthenticationServiceStartParams): AuthenticationServiceStartInternal { const apiKeys = new APIKeys({ clusterClient, logger: this.logger.get('api-key'), diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index f7f5f82d2858b..4f82c5653baa9 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -6,7 +6,11 @@ */ export { canRedirectRequest } from './can_redirect_request'; -export { AuthenticationService, AuthenticationServiceStart } from './authentication_service'; +export { + AuthenticationService, + AuthenticationServiceStart, + AuthenticationServiceStartInternal, +} from './authentication_service'; export { AuthenticationResult } from './authentication_result'; export { DeauthenticationResult } from './deauthentication_result'; export { diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx index 1e2588dafe233..c5adb3aff670e 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.tsx +++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx @@ -72,7 +72,7 @@ interface AuthorizationServiceStartParams { online$: Observable; } -export interface AuthorizationServiceSetup { +export interface AuthorizationServiceSetupInternal extends AuthorizationServiceSetup { actions: Actions; checkPrivilegesWithRequest: CheckPrivilegesWithRequest; checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; @@ -82,6 +82,21 @@ export interface AuthorizationServiceSetup { privileges: PrivilegesService; } +/** + * Authorization services available on the setup contract of the security plugin. + */ +export interface AuthorizationServiceSetup { + /** + * Actions are used to create the "actions" that are associated with Elasticsearch's + * application privileges, and are used to perform the authorization checks implemented + * by the various `checkPrivilegesWithRequest` derivatives + */ + actions: Actions; + checkPrivilegesWithRequest: CheckPrivilegesWithRequest; + checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; + mode: AuthorizationMode; +} + export class AuthorizationService { private logger!: Logger; private applicationName!: string; @@ -101,7 +116,7 @@ export class AuthorizationService { kibanaIndexName, getSpacesService, getCurrentUser, - }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { + }: AuthorizationServiceSetupParams): AuthorizationServiceSetupInternal { this.logger = loggers.get('authorization'); this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 16a3c2ae50058..4d67f3435e7da 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -6,6 +6,10 @@ */ export { Actions } from './actions'; -export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service'; +export { + AuthorizationService, + AuthorizationServiceSetup, + AuthorizationServiceSetupInternal, +} from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { CheckPrivilegesPayload } from './types'; diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index e50ab66a92547..a48c6833096cc 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -25,6 +25,7 @@ export type { InvalidateAPIKeysParams, InvalidateAPIKeyResult, GrantAPIKeyResult, + AuthenticationServiceStart, } from './authentication'; export type { CheckPrivilegesPayload } from './authorization'; export type AuthorizationServiceSetup = SecurityPluginStart['authz']; @@ -32,6 +33,7 @@ export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit'; export type { SecurityPluginSetup, SecurityPluginStart }; export type { AuthenticatedUser } from '../common/model'; export { ROUTE_TAG_CAN_REDIRECT } from './routes/tags'; +export { AuditServiceSetup } from './audit'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 57be308525fdd..98f1335b53450 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -10,7 +10,6 @@ import { combineLatest } from 'rxjs'; import { map } from 'rxjs/operators'; import type { TypeOf } from '@kbn/config-schema'; -import type { RecursiveReadonly } from '@kbn/utility-types'; import type { CoreSetup, CoreStart, @@ -36,10 +35,14 @@ import type { AnonymousAccessServiceStart } from './anonymous_access'; import { AnonymousAccessService } from './anonymous_access'; import type { AuditServiceSetup } from './audit'; import { AuditService, SecurityAuditLogger } from './audit'; -import type { AuthenticationServiceStart } from './authentication'; +import type { + AuthenticationServiceStart, + AuthenticationServiceStartInternal, +} from './authentication'; import { AuthenticationService } from './authentication'; import type { AuthorizationServiceSetup } from './authorization'; import { AuthorizationService } from './authorization'; +import type { AuthorizationServiceSetupInternal } from './authorization/authorization_service'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; import { ElasticsearchService } from './elasticsearch'; @@ -74,11 +77,14 @@ export interface SecurityPluginSetup { /** * @deprecated Use `authz` methods from the `SecurityServiceStart` contract instead. */ - authz: Pick< - AuthorizationServiceSetup, - 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' - >; + authz: AuthorizationServiceSetup; + /** + * Exposes information about the available security features under the current license. + */ license: SecurityLicense; + /** + * Exposes services for audit logging. + */ audit: AuditServiceSetup; } @@ -86,11 +92,14 @@ export interface SecurityPluginSetup { * Describes public Security plugin contract returned at the `start` stage. */ export interface SecurityPluginStart { - authc: Pick; - authz: Pick< - AuthorizationServiceSetup, - 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' - >; + /** + * Authentication services to confirm the user is who they say they are. + */ + authc: AuthenticationServiceStart; + /** + * Authorization services to manage and access the permissions a particular user has. + */ + authz: AuthorizationServiceSetup; } export interface PluginSetupDependencies { @@ -113,14 +122,9 @@ export interface PluginStartDependencies { * Represents Security Plugin instance that will be managed by the Kibana plugin system. */ export class SecurityPlugin - implements - Plugin< - RecursiveReadonly, - RecursiveReadonly, - PluginSetupDependencies - > { + implements Plugin { private readonly logger: Logger; - private authorizationSetup?: AuthorizationServiceSetup; + private authorizationSetup?: AuthorizationServiceSetupInternal; private auditSetup?: AuditServiceSetup; private anonymousAccessStart?: AnonymousAccessServiceStart; private configSubscription?: Subscription; @@ -152,7 +156,7 @@ export class SecurityPlugin private readonly authenticationService = new AuthenticationService( this.initializerContext.logger.get('authentication') ); - private authenticationStart?: AuthenticationServiceStart; + private authenticationStart?: AuthenticationServiceStartInternal; private readonly getAuthentication = () => { if (!this.authenticationStart) { throw new Error(`authenticationStart is not registered!`); diff --git a/x-pack/plugins/security/server/routes/api_keys/create.test.ts b/x-pack/plugins/security/server/routes/api_keys/create.test.ts index 502a7cb1246c4..ee28681adbd5f 100644 --- a/x-pack/plugins/security/server/routes/api_keys/create.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/create.test.ts @@ -12,7 +12,7 @@ import type { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRequestHandlerContext } from '../../types'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -28,7 +28,7 @@ describe('Create API Key route', () => { } let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { authc = authenticationServiceMock.createStart(); const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts index da097faf8c6b2..1000e79563b57 100644 --- a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -12,7 +12,7 @@ import type { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRequestHandlerContext } from '../../types'; import { routeDefinitionParamsMock } from '../index.mock'; @@ -28,7 +28,7 @@ describe('API keys enabled', () => { } let routeHandler: RequestHandler; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { authc = authenticationServiceMock.createStart(); const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 1e0b6784a45c8..5acc5817bfb3e 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -13,7 +13,7 @@ import { httpServerMock } from 'src/core/server/mocks'; import type { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { AuthenticationResult, DeauthenticationResult, @@ -28,7 +28,7 @@ import { defineCommonRoutes } from './common'; describe('Common authentication routes', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; let license: jest.Mocked; let mockContext: SecurityRequestHandlerContext; beforeEach(() => { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index 3b2497ed9f30b..c28c435217ef3 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -11,7 +11,7 @@ import type { RequestHandler, RouteConfig } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStart } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication'; import { AuthenticationResult, SAMLLogin } from '../../authentication'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { SecurityRouter } from '../../types'; @@ -21,7 +21,7 @@ import { defineSAMLRoutes } from './saml'; describe('SAML authentication routes', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index cdb3a2fa289fd..e36ca1b9ab72e 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -10,8 +10,8 @@ import type { HttpResources, IBasePath, Logger } from 'src/core/server'; import type { KibanaFeature } from '../../../features/server'; import type { SecurityLicense } from '../../common/licensing'; -import type { AuthenticationServiceStart } from '../authentication'; -import type { AuthorizationServiceSetup } from '../authorization'; +import type { AuthenticationServiceStartInternal } from '../authentication'; +import type { AuthorizationServiceSetupInternal } from '../authorization'; import type { ConfigType } from '../config'; import type { SecurityFeatureUsageServiceStart } from '../feature_usage'; import type { Session } from '../session_management'; @@ -34,12 +34,12 @@ export interface RouteDefinitionParams { httpResources: HttpResources; logger: Logger; config: ConfigType; - authz: AuthorizationServiceSetup; + authz: AuthorizationServiceSetupInternal; getSession: () => PublicMethodsOf; license: SecurityLicense; getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; - getAuthenticationService: () => AuthenticationServiceStart; + getAuthenticationService: () => AuthenticationServiceStartInternal; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index a97fce7ea4b19..e5f4381a20421 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -15,8 +15,8 @@ import { kibanaResponseFactory } from 'src/core/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; -import type { AuthenticationServiceStart } from '../../authentication'; import { AuthenticationResult } from '../../authentication'; +import type { AuthenticationServiceStartInternal } from '../../authentication/authentication_service'; import { authenticationServiceMock } from '../../authentication/authentication_service.mock'; import type { Session } from '../../session_management'; import { sessionMock } from '../../session_management/session.mock'; @@ -26,7 +26,7 @@ import { defineChangeUserPasswordRoutes } from './change_password'; describe('Change password', () => { let router: jest.Mocked; - let authc: DeeplyMockedKeys; + let authc: DeeplyMockedKeys; let session: jest.Mocked>; let routeHandler: RequestHandler; let routeConfig: RouteConfig; diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index e052fd1c7ab6a..837b3c594d396 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -9,7 +9,7 @@ import type { CoreSetup, LegacyRequest } from 'src/core/server'; import { KibanaRequest, SavedObjectsClient } from '../../../../../src/core/server'; import type { AuditServiceSetup, SecurityAuditLogger } from '../audit'; -import type { AuthorizationServiceSetup } from '../authorization'; +import type { AuthorizationServiceSetupInternal } from '../authorization/authorization_service'; import type { SpacesService } from '../plugin'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; @@ -17,7 +17,7 @@ interface SetupSavedObjectsParams { legacyAuditLogger: SecurityAuditLogger; audit: AuditServiceSetup; authz: Pick< - AuthorizationServiceSetup, + AuthorizationServiceSetupInternal, 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest' >; savedObjects: CoreSetup['savedObjects']; diff --git a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts index 0b8a7abab2382..bc7cb727edd80 100644 --- a/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/spaces/secure_spaces_client_wrapper.test.ts @@ -15,7 +15,10 @@ import { spacesClientMock } from '../../../spaces/server/mocks'; import type { AuditEvent, AuditLogger } from '../audit'; import { SpaceAuditAction } from '../audit'; import { auditServiceMock } from '../audit/index.mock'; -import type { AuthorizationServiceSetup } from '../authorization'; +import type { + AuthorizationServiceSetup, + AuthorizationServiceSetupInternal, +} from '../authorization'; import { authorizationMock } from '../authorization/index.mock'; import type { CheckPrivilegesResponse } from '../authorization/types'; import type { LegacySpacesAuditLogger } from './legacy_audit_logger'; @@ -85,7 +88,9 @@ const setup = ({ securityEnabled = false }: Opts = {}) => { }; }; -const expectNoAuthorizationCheck = (authorization: jest.Mocked) => { +const expectNoAuthorizationCheck = ( + authorization: jest.Mocked +) => { expect(authorization.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled(); expect(authorization.checkPrivilegesWithRequest).not.toHaveBeenCalled(); expect(authorization.checkSavedObjectsPrivilegesWithRequest).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index e006944eb914f..1e0d798cf7f07 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1114,3 +1114,15 @@ export interface GetExceptionSummaryResponse { macos: number; linux: number; } + +/** + * Supported React-Router state for the Generic List page + */ +export interface ListPageRouteState { + /** Where the user should be redirected to when the `Back` button is clicked */ + onBackButtonNavigateTo: Parameters; + /** The URL for the `Back` button */ + backButtonUrl?: string; + /** The label for the button */ + backButtonLabel?: string; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 8d66370fea4d3..94a2e7f236beb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -6,7 +6,6 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { ApplicationStart } from 'kibana/public'; import { DeleteTrustedAppsRequestSchema, @@ -133,15 +132,3 @@ export type TrustedApp = NewTrustedApp & { updated_at: string; updated_by: string; }; - -/** - * Supported React-Router state for the Trusted Apps List page - */ -export interface TrustedAppsListPageRouteState { - /** Where the user should be redirected to when the `Back` button is clicked */ - onBackButtonNavigateTo: Parameters; - /** The URL for the `Back` button */ - backButtonUrl?: string; - /** The label for the button */ - backButtonLabel?: string; -} diff --git a/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/back_to_external_app_button.tsx b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/back_to_external_app_button.tsx new file mode 100644 index 0000000000000..78c854d933584 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/back_to_external_app_button.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { memo } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import styled from 'styled-components'; + +import { ListPageRouteState } from '../../../../common/endpoint/types'; + +import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; + +const EuiButtonEmptyStyled = styled(EuiButtonEmpty)` + margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; + + .euiIcon { + width: ${({ theme }) => theme.eui.euiIconSizes.small}; + height: ${({ theme }) => theme.eui.euiIconSizes.small}; + } + + .text { + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + } +`; + +export const BackToExternalAppButton = memo( + ({ backButtonLabel, backButtonUrl, onBackButtonNavigateTo }) => { + const handleBackOnClick = useNavigateToAppEventHandler(...onBackButtonNavigateTo!); + + return ( + + {backButtonLabel || ( + + )} + + ); + } +); + +BackToExternalAppButton.displayName = 'BackToExternalAppButton'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/index.ts similarity index 75% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts rename to x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/index.ts index 644ada96a9f05..d4a2f8de13546 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/types.ts +++ b/x-pack/plugins/security_solution/public/management/components/back_to_external_app_button/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { Phases } from '../../../common/types'; - -export type Phase = keyof Phases; +export { BackToExternalAppButton } from './back_to_external_app_button'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx index 048bd97664f2e..4171441e7de25 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.test.tsx @@ -111,6 +111,23 @@ describe('Event filter form', () => { expect(store.getState()!.management!.eventFilters!.form!.hasNameError).toBeFalsy(); }); + it('should change name with a white space still shows an error', async () => { + component = renderComponentWithdata(); + + const nameInput = component.getByPlaceholderText(NAME_PLACEHOLDER); + + act(() => { + fireEvent.change(nameInput, { + target: { + value: ' ', + }, + }); + }); + + expect(store.getState()!.management!.eventFilters!.form!.entry!.name).toBe(''); + expect(store.getState()!.management!.eventFilters!.form!.hasNameError).toBeTruthy(); + }); + it('should change comments', async () => { component = renderComponentWithdata(); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 93658d4efa737..121808b62f570 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -91,11 +91,12 @@ export const EventFiltersForm: React.FC = memo( const handleOnChangeName = useCallback( (e: React.ChangeEvent) => { if (!exception) return; + const name = e.target.value.toString().trim(); dispatch({ type: 'eventFiltersChangeForm', payload: { - entry: { ...exception, name: e.target.value.toString() }, - hasNameError: !e.target.value, + entry: { ...exception, name }, + hasNameError: !name, }, }); }, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 465f92dfda767..59d409874c561 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -175,4 +175,30 @@ describe('When on the Event Filters List Page', () => { }); }); }); + + describe('and the back button is present', () => { + beforeEach(async () => { + renderResult = render(); + act(() => { + history.push('/event_filters', { + onBackButtonNavigateTo: [{ appId: 'appId' }], + backButtonLabel: 'back to fleet', + backButtonUrl: '/fleet', + }); + }); + }); + + it('back button is present', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + + it('back button is not present', () => { + act(() => { + history.push('/event_filters'); + }); + expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 32fc018210418..00ee80c5d7022 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { memo, useCallback, useEffect } from 'react'; +import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiSpacer, EuiHorizontalRule, EuiText } from '@elastic/eui'; @@ -34,7 +34,7 @@ import { showDeleteModal, } from '../store/selector'; import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content'; -import { Immutable } from '../../../../../common/endpoint/types'; +import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types'; import { ExceptionItem, ExceptionItemProps, @@ -42,6 +42,7 @@ import { import { EventFilterDeleteModal } from './components/event_filter_delete_modal'; import { SearchBar } from '../../../components/search_bar'; +import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; type EventListPaginatedContent = PaginatedContentProps< Immutable, @@ -59,6 +60,7 @@ const AdministrationListPage = styled(_AdministrationListPage)` `; export const EventFiltersListPage = memo(() => { + const { state: routeState } = useLocation(); const history = useHistory(); const dispatch = useDispatch>(); const isActionError = useEventFiltersSelector(getActionError); @@ -103,6 +105,13 @@ export const EventFiltersListPage = memo(() => { } }, [dispatch, formEntry, history, isActionError, location, navigateCallback]); + const backButton = useMemo(() => { + if (routeState && routeState.onBackButtonNavigateTo) { + return ; + } + return null; + }, [routeState]); + const handleAddButtonClick = useCallback( () => navigateCallback({ @@ -173,6 +182,7 @@ export const EventFiltersListPage = memo(() => { return ( ( }; }, [eventFiltersApi, toasts]); - const eventFiltersRouteState = useMemo(() => { + const eventFiltersRouteState = useMemo(() => { const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; return { backButtonLabel: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index b1464d23e00fb..ed3ba10c1e62b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -17,7 +17,7 @@ import { import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { getTrustedAppsListPath } from '../../../../../../common/routing'; import { - TrustedAppsListPageRouteState, + ListPageRouteState, GetExceptionSummaryResponse, } from '../../../../../../../../common/endpoint/types'; import { PLUGIN_ID as FLEET_PLUGIN_ID } from '../../../../../../../../../fleet/common'; @@ -67,7 +67,7 @@ export const FleetTrustedAppsCard = memo(( }, [toasts, trustedAppsApi]); const trustedAppsListUrlPath = getTrustedAppsListPath(); - const trustedAppRouteState = useMemo(() => { + const trustedAppRouteState = useMemo(() => { const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`; return { backButtonLabel: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index fac9fb1e5bf6e..691601b69e3cd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -900,4 +900,34 @@ describe('When on the Trusted Apps Page', () => { }); }); }); + + describe('and the back button is present', () => { + let renderResult: ReturnType; + beforeEach(async () => { + renderResult = render(); + await act(async () => { + await waitForAction('trustedAppsListResourceStateChanged'); + }); + reactTestingLibrary.act(() => { + history.push('/trusted_apps', { + onBackButtonNavigateTo: [{ appId: 'appId' }], + backButtonLabel: 'back to fleet', + backButtonUrl: '/fleet', + }); + }); + }); + + it('back button is present', () => { + const button = renderResult.queryByTestId('backToOrigin'); + expect(button).not.toBeNull(); + expect(button).toHaveAttribute('href', '/fleet'); + }); + + it('back button is not present', () => { + reactTestingLibrary.act(() => { + history.push('/trusted_apps'); + }); + expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 5603b8e2d61c9..4cd6ad62f3a35 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -7,11 +7,9 @@ import React, { memo, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, - EuiButtonEmpty, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -35,14 +33,14 @@ import { TrustedAppsGrid } from './components/trusted_apps_grid'; import { TrustedAppsList } from './components/trusted_apps_list'; import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; import { TrustedAppsNotifications } from './trusted_apps_notifications'; -import { TrustedAppsListPageRouteState } from '../../../../../common/endpoint/types'; -import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; import { EmptyState } from './components/empty_state'; import { SearchBar } from '../../../components/search_bar'; +import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; +import { ListPageRouteState } from '../../../../../common/endpoint/types'; export const TrustedAppsPage = memo(() => { - const { state: routeState } = useLocation(); + const { state: routeState } = useLocation(); const location = useTrustedAppsSelector(getCurrentLocation); const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); @@ -161,43 +159,3 @@ export const TrustedAppsPage = memo(() => { }); TrustedAppsPage.displayName = 'TrustedAppsPage'; - -const EuiButtonEmptyStyled = styled(EuiButtonEmpty)` - margin-bottom: ${({ theme }) => theme.eui.euiSizeS}; - - .euiIcon { - width: ${({ theme }) => theme.eui.euiIconSizes.small}; - height: ${({ theme }) => theme.eui.euiIconSizes.small}; - } - - .text { - font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; - } -`; - -const BackToExternalAppButton = memo( - ({ backButtonLabel, backButtonUrl, onBackButtonNavigateTo }) => { - const handleBackOnClick = useNavigateToAppEventHandler(...onBackButtonNavigateTo!); - - return ( - - {backButtonLabel || ( - - )} - - ); - } -); - -BackToExternalAppButton.displayName = 'BackToExternalAppButton'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh new file mode 100755 index 0000000000000..d78726f9e23b5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example delete all rules +# ./delete_rules_by_query.sh + +# Example delete rules with tag "test" +# ./delete_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"delete\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh new file mode 100755 index 0000000000000..1c7f05f4057cf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example disable all rules +# ./disable_rules_by_query.sh + +# Example disable rules with tag "test" +# ./disable_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"disable\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh new file mode 100755 index 0000000000000..13da480d74f83 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example duplicate all rules +# ./duplicate_rules_by_query.sh + +# Example duplicate rules with tag "test" +# ./duplicate_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"duplicate\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh new file mode 100755 index 0000000000000..52bb51b6dd6df --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example enable all rules +# ./enable_rules_by_query.sh + +# Example enable rules with tag "test" +# ./enable_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"enable\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh new file mode 100755 index 0000000000000..954fc2104262b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example export all rules +# ./export_rules_by_query.sh + +# Example export rules with tag "test" +# ./export_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"export\" +}" diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 879c666cff672..4bc03b4ba6e2a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2879,8 +2879,6 @@ "indexPatternManagement.createIndexPattern.betaLabel": "ベータ", "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", "indexPatternManagement.createIndexPattern.documentation": "ドキュメンテーションを表示", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "この機能にはベーシックライセンスが必要です。", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "新規データを確認", "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "一部のインデックスは表示されない場合があります。{link}してください。", "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "インデックスパターンを作成します", @@ -5245,7 +5243,6 @@ "xpack.apm.agentConfig.chooseService.editButton": "編集", "xpack.apm.agentConfig.chooseService.service.environment.label": "環境", "xpack.apm.agentConfig.chooseService.service.name.label": "サービス名", - "xpack.apm.agentConfig.chooseService.title": "サービスを選択", "xpack.apm.agentConfig.circuitBreakerEnabled.description": "Circuit Breaker を有効にすべきかどうかを指定するブール値。 有効にすると、エージェントは定期的にストレス監視をポーリングして、システム/プロセス/JVMのストレス状態を検出します。監視のいずれかがストレスの兆候を検出した場合、`recording`構成オプションの設定が「false」であるかのようにエージェントは一時停止し、リソース消費を最小限に抑えられます。一時停止した場合、エージェントはストレス状態が緩和されたかどうかを検出するために同じ監視のポーリングを継続します。すべての監視でシステム/プロセス/JVMにストレスがないことが認められると、エージェントは再開して完全に機能します。", "xpack.apm.agentConfig.circuitBreakerEnabled.label": "Cirtcuit Breaker が有効", "xpack.apm.agentConfig.configTable.appliedTooltipMessage": "1 つ以上のエージェントにより適用されました", @@ -5304,8 +5301,6 @@ "xpack.apm.agentConfig.servicePage.service.description": "構成するサービスを選択してください。", "xpack.apm.agentConfig.servicePage.service.fieldLabel": "サービス名", "xpack.apm.agentConfig.servicePage.service.title": "サービス", - "xpack.apm.agentConfig.servicePage.title": "サービスを選択", - "xpack.apm.agentConfig.settings.title": "構成オプション", "xpack.apm.agentConfig.settingsPage.discardChangesButton": "変更を破棄", "xpack.apm.agentConfig.settingsPage.notFound.message": "リクエストされた構成が存在しません", "xpack.apm.agentConfig.settingsPage.notFound.title": "申し訳ございません、エラーが発生しました", @@ -5324,7 +5319,6 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuReliefThreshold.label": "ストレス監視システム CPU 緩和しきい値", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "システムCPU監視でシステムCPUストレスの検出に使用するしきい値。システムCPUが少なくとも「stress_monitor_cpu_duration_threshold」と同じ長さ以上の期間にわたってこのしきい値を超えると、監視機能はこれをストレス状態と見なします。", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "ストレス監視システム CPU ストレスしきい値", - "xpack.apm.agentConfig.titleText": "エージェント中央構成", "xpack.apm.agentConfig.transactionIgnoreUrl.description": "特定の URL への要求が命令されないように制限するために使用します。この構成では、無視される URL パスのワイルドカードパターンのカンマ区切りのリストを使用できます。受信 HTTP 要求が検出されると、要求パスが、リストの各要素に対してテストされます。たとえば、このリストに「/home/index」を追加すると、一致して、「http://localhost/home/index」と「http://whatever.com/home/index?value1=123」から命令が削除されます。", "xpack.apm.agentConfig.transactionIgnoreUrl.label": "URL に基づくトランザクションを無視", "xpack.apm.agentConfig.transactionMaxSpans.description": "トランザクションごとに記録される範囲を制限します。", @@ -5390,9 +5384,6 @@ "xpack.apm.apply.label": "適用", "xpack.apm.applyFilter": "{title} フィルターを適用", "xpack.apm.applyOptions": "オプションを適用", - "xpack.apm.breadcrumb.serviceMapTitle": "サービスマップ", - "xpack.apm.breadcrumb.servicesTitle": "サービス", - "xpack.apm.breadcrumb.tracesTitle": "トレース", "xpack.apm.chart.annotation.version": "バージョン", "xpack.apm.chart.cpuSeries.processAverageLabel": "プロセス平均", "xpack.apm.chart.cpuSeries.processMaxLabel": "プロセス最大", @@ -5785,7 +5776,6 @@ "xpack.apm.settings.anomalyDetection.addEnvironments.selectorLabel": "環境", "xpack.apm.settings.anomalyDetection.addEnvironments.selectorPlaceholder": "環境を選択または追加", "xpack.apm.settings.anomalyDetection.addEnvironments.titleText": "環境を選択", - "xpack.apm.settings.anomalyDetection.descriptionText": "機械学習異常検知統合により、レイテンシの異常を特定することで、各構成された環境で、サービスのアプリケーション正常性ステータスインジケーターが有効になります。", "xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel": "アクション", "xpack.apm.settings.anomalyDetection.jobList.addEnvironments": "MLジョブを作成", "xpack.apm.settings.anomalyDetection.jobList.emptyListText": "異常検知ジョブがありません。", @@ -5795,7 +5785,6 @@ "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText": "異常検知を新しい環境に追加するには、機械学習ジョブを作成します。既存の機械学習ジョブは、{mlJobsLink}で管理できます。", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText": "機械学習", "xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText": "MLでジョブを表示", - "xpack.apm.settings.anomalyDetection.titleText": "異常検知", "xpack.apm.settings.apmIndices.applyButton": "変更を適用", "xpack.apm.settings.apmIndices.applyChanges.failed.text": "インデックスの適用時に何か問題が発生しました。エラー:{errorMessage}", "xpack.apm.settings.apmIndices.applyChanges.failed.title": "インデックスが適用できませんでした。", @@ -5813,8 +5802,6 @@ "xpack.apm.settings.apmIndices.title": "インデックス", "xpack.apm.settings.apmIndices.transactionIndicesLabel": "トランザクションインデックス", "xpack.apm.settings.customizeApp": "アプリをカスタマイズ", - "xpack.apm.settings.customizeApp.description": "次の設定でAPMアプリ経験を拡張します。", - "xpack.apm.settings.customizeApp.title": "アプリをカスタマイズ", "xpack.apm.settings.customizeUI.customLink": "カスタムリンク", "xpack.apm.settings.customizeUI.customLink.create.failed": "リンクを保存できませんでした!", "xpack.apm.settings.customizeUI.customLink.create.failed.message": "リンクを保存するときに問題が発生しました。エラー:「{errorMessage}」", @@ -5863,7 +5850,6 @@ "xpack.apm.settings.customizeUI.customLink.table.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "インデックス", - "xpack.apm.settings.pageTitle": "設定", "xpack.apm.settingsLinkLabel": "設定", "xpack.apm.setupInstructionsButtonLabel": "セットアップの手順", "xpack.apm.significanTerms.license.text": "相関関係APIを使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。", @@ -14670,8 +14656,6 @@ "xpack.ml.fieldTypeIcon.numberTypeAriaLabel": "数字タイプ", "xpack.ml.fieldTypeIcon.textTypeAriaLabel": "テキストタイプ", "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "不明なタイプ", - "xpack.ml.fileDataVisualizerDescription": "CSV、NDJSON、またはログファイルをインポートします。", - "xpack.ml.fileDataVisualizerTitle": "ファイルをアップロード", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "実際値が通常値と同じ", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "100x よりも高い", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "100x よりも低い", @@ -21393,7 +21377,6 @@ "xpack.securitySolution.trustedapps.list.actions.delete": "削除", "xpack.securitySolution.trustedapps.list.actions.delete.description": "このエントリを削除", "xpack.securitySolution.trustedapps.list.addButton": "信頼できるアプリケーションを追加", - "xpack.securitySolution.trustedapps.list.backButton": "戻る", "xpack.securitySolution.trustedapps.list.columns.actions": "アクション", "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", "xpack.securitySolution.trustedapps.listEmptyState.message": "現在、エンドポイントには信頼できるアプリケーションがありません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ac1a8d69796a2..46bdb83c42528 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2893,8 +2893,6 @@ "indexPatternManagement.createIndexPattern.betaLabel": "公测版", "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", "indexPatternManagement.createIndexPattern.documentation": "阅读文档", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "此功能需要基本级许可证。", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本级", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "检查新数据", "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "部分索引可能已隐藏。仍然尝试{link}。", "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "创建索引模式", @@ -5274,7 +5272,6 @@ "xpack.apm.agentConfig.chooseService.editButton": "编辑", "xpack.apm.agentConfig.chooseService.service.environment.label": "环境", "xpack.apm.agentConfig.chooseService.service.name.label": "服务名称", - "xpack.apm.agentConfig.chooseService.title": "选择服务", "xpack.apm.agentConfig.circuitBreakerEnabled.description": "指定是否应启用断路器的布尔值。 启用时,代理定期轮询压力监测以检测系统/进程/JVM 压力状态。如果任意监测检测到压力迹象,代理将会暂停,就如 `recording` 配置选项已设置为 `false` 一样,从而使资源消耗降低至最小程度。暂停时,代理继续轮询相同的监测,以便检测压力状态是否已缓解。如果所有监测认为系统/进程/JVM 不再承受压力时,代理将完全恢复正常运行。", "xpack.apm.agentConfig.circuitBreakerEnabled.label": "断路器已启用", "xpack.apm.agentConfig.configTable.appliedTooltipMessage": "已至少由一个代理应用", @@ -5333,8 +5330,6 @@ "xpack.apm.agentConfig.servicePage.service.description": "选择要配置的服务。", "xpack.apm.agentConfig.servicePage.service.fieldLabel": "服务名称", "xpack.apm.agentConfig.servicePage.service.title": "服务", - "xpack.apm.agentConfig.servicePage.title": "选择服务", - "xpack.apm.agentConfig.settings.title": "配置选项", "xpack.apm.agentConfig.settingsPage.discardChangesButton": "放弃更改", "xpack.apm.agentConfig.settingsPage.notFound.message": "请求的配置不存在", "xpack.apm.agentConfig.settingsPage.notFound.title": "抱歉,有错误", @@ -5353,7 +5348,6 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuReliefThreshold.label": "压力监测系统 cpu 缓解阈值", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "系统 CPU 监测用于检测系统 CPU 压力的阈值。如果系统 CPU 超过此阈值的持续时间至少有 `stress_monitor_cpu_duration_threshold`,监测会将其视为压力状态。", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "压力监测系统 cpu 压力阈值", - "xpack.apm.agentConfig.titleText": "代理中央配置", "xpack.apm.agentConfig.transactionIgnoreUrl.description": "用于限制对某些 URL 的请求不被检测。此配置接受应忽略的 URL 路径的通配符模式逗号分隔列表。当监测到传入 HTTP 请求时,会根据此列表中的每个元素测试其请求路径。例如,将 `/home/index` 添加到此列表中后,该元素将匹配并删除 `http://localhost/home/index` 和 `http://whatever.com/home/index?value1=123` 的检测", "xpack.apm.agentConfig.transactionIgnoreUrl.label": "基于 URL 忽略事务", "xpack.apm.agentConfig.transactionMaxSpans.description": "限制每个事务记录的跨度数量。", @@ -5419,9 +5413,6 @@ "xpack.apm.apply.label": "应用", "xpack.apm.applyFilter": "应用 {title} 筛选", "xpack.apm.applyOptions": "应用选项", - "xpack.apm.breadcrumb.serviceMapTitle": "服务地图", - "xpack.apm.breadcrumb.servicesTitle": "服务", - "xpack.apm.breadcrumb.tracesTitle": "追溯", "xpack.apm.chart.annotation.version": "版本", "xpack.apm.chart.cpuSeries.processAverageLabel": "进程平均值", "xpack.apm.chart.cpuSeries.processMaxLabel": "进程最大值", @@ -5819,7 +5810,6 @@ "xpack.apm.settings.anomalyDetection.addEnvironments.selectorLabel": "环境", "xpack.apm.settings.anomalyDetection.addEnvironments.selectorPlaceholder": "选择或添加环境", "xpack.apm.settings.anomalyDetection.addEnvironments.titleText": "选择环境", - "xpack.apm.settings.anomalyDetection.descriptionText": "Machine Learning 异常检测集成通过识别延迟异常来为每个已配置环境中的服务启用应用程序运行状态指标。", "xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel": "操作", "xpack.apm.settings.anomalyDetection.jobList.addEnvironments": "创建 ML 作业", "xpack.apm.settings.anomalyDetection.jobList.emptyListText": "无异常检测作业。", @@ -5829,7 +5819,6 @@ "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText": "要将异常检测添加到新环境,请创建 Machine Learning 作业。现有 Machine Learning 作业可在 {mlJobsLink} 中进行管理。", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText": "Machine Learning", "xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText": "在 ML 中查看作业", - "xpack.apm.settings.anomalyDetection.titleText": "异常检测", "xpack.apm.settings.apmIndices.applyButton": "应用更改", "xpack.apm.settings.apmIndices.applyChanges.failed.text": "应用索引时出现问题。错误:{errorMessage}", "xpack.apm.settings.apmIndices.applyChanges.failed.title": "无法应用索引。", @@ -5847,8 +5836,6 @@ "xpack.apm.settings.apmIndices.title": "索引", "xpack.apm.settings.apmIndices.transactionIndicesLabel": "事务索引", "xpack.apm.settings.customizeApp": "定制应用", - "xpack.apm.settings.customizeApp.description": "使用以下设置扩展 APM 应用体验。", - "xpack.apm.settings.customizeApp.title": "定制应用", "xpack.apm.settings.customizeUI.customLink": "定制链接", "xpack.apm.settings.customizeUI.customLink.create.failed": "链接无法保存!", "xpack.apm.settings.customizeUI.customLink.create.failed.message": "保存链接时出现了问题。错误:“{errorMessage}”", @@ -5897,7 +5884,6 @@ "xpack.apm.settings.customizeUI.customLink.table.noResultFound": "没有“{value}”的结果。", "xpack.apm.settings.customizeUI.customLink.table.url": "URL", "xpack.apm.settings.indices": "索引", - "xpack.apm.settings.pageTitle": "设置", "xpack.apm.settingsLinkLabel": "设置", "xpack.apm.setupInstructionsButtonLabel": "设置说明", "xpack.apm.significanTerms.license.text": "要使用相关性 API,必须订阅 Elastic 白金级许可证。", @@ -14863,8 +14849,6 @@ "xpack.ml.fieldTypeIcon.numberTypeAriaLabel": "数字类型", "xpack.ml.fieldTypeIcon.textTypeAriaLabel": "文本类型", "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "未知类型", - "xpack.ml.fileDataVisualizerDescription": "导入您自己的 CSV、NDJSON 或日志文件。", - "xpack.ml.fileDataVisualizerTitle": "上传文件", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "实际上与典型模式相同", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "高 100 多倍", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "低 100 多倍", @@ -21728,7 +21712,6 @@ "xpack.securitySolution.trustedapps.list.actions.delete": "移除", "xpack.securitySolution.trustedapps.list.actions.delete.description": "移除此条目", "xpack.securitySolution.trustedapps.list.addButton": "添加受信任的应用程序", - "xpack.securitySolution.trustedapps.list.backButton": "返回", "xpack.securitySolution.trustedapps.list.columns.actions": "操作", "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", "xpack.securitySolution.trustedapps.list.totalCount": "{totalItemCount, plural, other {# 个受信任的应用程序}}", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index e02cf44b0856e..c34da1bc097bc 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -12,7 +12,8 @@ import { PluginInitializerContext, AppMountParameters, } from 'kibana/public'; -import { of } from 'rxjs'; +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -104,32 +105,41 @@ export class UptimePlugin }); plugins.observability.navigation.registerSections( - of([ - { - label: 'Uptime', - sortKey: 200, - entries: [ - { - label: i18n.translate('xpack.uptime.overview.heading', { - defaultMessage: 'Monitoring overview', - }), - app: 'uptime', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - { - label: i18n.translate('xpack.uptime.certificatesPage.heading', { - defaultMessage: 'TLS Certificates', - }), - app: 'uptime', - path: '/certificates', - matchFullPath: true, - }, - ], - }, - ]) + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Uptime', + sortKey: 200, + entries: [ + { + label: i18n.translate('xpack.uptime.overview.heading', { + defaultMessage: 'Monitoring overview', + }), + app: 'uptime', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + { + label: i18n.translate('xpack.uptime.certificatesPage.heading', { + defaultMessage: 'TLS Certificates', + }), + app: 'uptime', + path: '/certificates', + matchFullPath: true, + }, + ], + }, + ]; + } + + return []; + }) + ) ); + core.application.register({ id: PLUGIN.ID, euiIconType: 'logoObservability', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 40c0fe398bc57..c1f6bcb9e1510 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -149,13 +149,17 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`); + validateInstanceEvent(event, `created new instance: 'instance'`, false); break; case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`); + validateInstanceEvent(event, `instance 'instance' has recovered`, true); break; case 'active-instance': - validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup: 'default'`, + false + ); break; // this will get triggered as we add new event actions default: @@ -163,7 +167,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } } - function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { spaceId: Spaces.space1.id, savedObjects: [ @@ -172,6 +180,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, }); } }); @@ -288,10 +297,10 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`); + validateInstanceEvent(event, `created new instance: 'instance'`, false); break; case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`); + validateInstanceEvent(event, `instance 'instance' has recovered`, true); break; case 'active-instance': expect( @@ -299,7 +308,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ).to.be(true); validateInstanceEvent( event, - `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'` + `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, + false ); break; // this will get triggered as we add new event actions @@ -308,7 +318,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } } - function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { spaceId: Spaces.space1.id, savedObjects: [ @@ -317,6 +331,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, }); } }); @@ -376,6 +391,7 @@ interface ValidateEventLogParams { savedObjects: SavedObject[]; outcome?: string; message: string; + shouldHaveEventEnd?: boolean; errorMessage?: string; status?: string; actionGroupId?: string; @@ -385,7 +401,7 @@ interface ValidateEventLogParams { export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { const { spaceId, savedObjects, outcome, message, errorMessage } = params; - const { status, actionGroupId, instanceId, reason } = params; + const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params; if (status) { expect(event?.kibana?.alerting?.status).to.be(status); @@ -411,16 +427,23 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa if (duration !== undefined) { expect(typeof duration).to.be('number'); expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); - const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) - ); + if (shouldHaveEventEnd !== false) { + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); - // account for rounding errors - expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + } + + if (shouldHaveEventEnd === false) { + expect(eventEnd).not.to.be.ok(); + } } expect(event?.event?.outcome).to.equal(outcome); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts new file mode 100644 index 0000000000000..2b92562b9bde5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +// eslint-disable-next-line import/no-default-export +export default function eventLogAlertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('eventLog alerts', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should generate expected alert events for normal operation', async () => { + // pattern of when the alert should fire + const pattern = { + instance: [false, true, true, false, false, true, true, true], + }; + + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [], + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); + + // wait for the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute', { gte: 9 }], + ['new-instance', { equal: 2 }], + ['active-instance', { gte: 4 }], + ['recovered-instance', { equal: 2 }], + ]), + }); + }); + + // filter out the execute event actions + const instanceEvents = events.filter( + (event: IValidatedEvent) => event?.event?.action !== 'execute' + ); + + const currentAlertSpan: { + alertId?: string; + start?: string; + durationToDate?: number; + } = {}; + for (let i = 0; i < instanceEvents.length; ++i) { + switch (instanceEvents[i]?.event?.action) { + case 'new-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + // a new alert should generate a unique UUID for the duration of its activeness + expect(instanceEvents[i]?.event?.end).to.be(undefined); + + currentAlertSpan.alertId = instanceEvents[i]?.kibana?.alerting?.instance_id; + currentAlertSpan.start = instanceEvents[i]?.event?.start; + currentAlertSpan.durationToDate = instanceEvents[i]?.event?.duration; + break; + + case 'active-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); + expect(instanceEvents[i]?.event?.end).to.be(undefined); + + if (instanceEvents[i]?.event?.duration! !== 0) { + expect(instanceEvents[i]?.event?.duration! > currentAlertSpan.durationToDate!).to.be( + true + ); + } + currentAlertSpan.durationToDate = instanceEvents[i]?.event?.duration; + break; + + case 'recovered-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); + expect(instanceEvents[i]?.event?.end).not.to.be(undefined); + expect( + new Date(instanceEvents[i]?.event?.end!).valueOf() - + new Date(instanceEvents[i]?.event?.start!).valueOf() + ).to.equal(instanceEvents[i]?.event?.duration! / 1000 / 1000); + break; + } + } + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 9154c85af1bc7..318dfdfe065df 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -76,7 +76,9 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); alertInstances.forEach(([key, value], index) => { expect(key).to.eql(`instance-${index}`); - expect(value.state).to.eql({ instanceStateValue: true }); + expect(value.state.instanceStateValue).to.be(true); + expect(value.state.start).not.to.be(undefined); + expect(value.state.duration).not.to.be(undefined); }); }); @@ -131,7 +133,9 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); alertInstances.forEach(([key, value], index) => { expect(key).to.eql(`instance-${index}`); - expect(value.state).to.eql({ instanceStateValue: true }); + expect(value.state.instanceStateValue).to.be(true); + expect(value.state.start).not.to.be(undefined); + expect(value.state.duration).not.to.be(undefined); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index e9aeec1717c96..5c3374a4d9c70 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -37,6 +37,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./builtin_alert_types')); loadTestFile(require.resolve('./mustache_templates.ts')); loadTestFile(require.resolve('./notify_when')); + loadTestFile(require.resolve('./event_log_alerts')); // note that this test will destroy existing spaces loadTestFile(require.resolve('./migrations')); diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index f048bf47991f2..f499c5bf0cfe8 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -143,6 +143,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(styleObj['background-color']).to.be('rgb(168, 191, 218)'); // should also set text color when in cell mode expect(styleObj.color).to.be('rgb(0, 0, 0)'); + await PageObjects.lens.closeTablePalettePanel(); + }); + + it('should allow to show a summary table for metric columns', async () => { + await PageObjects.lens.setTableSummaryRowFunction('sum'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertExactText( + '[data-test-subj="lnsDataTable-footer-169.228.188.120-›-Average-of-bytes"]', + 'Sum: 18,994' + ); }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index bbc7f5992506b..18f4f6a38a7b1 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -35,14 +35,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 280801d1becb5..431c0550b9271 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -41,16 +41,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.error.expectForbidden(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should not display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.navigateToKibanaHome(); await ml.navigation.openKibanaNav(); await ml.testExecution.logTestStep('should not display the ML nav link'); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index dbf467e998f25..a53ed2fafe30c 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -35,14 +35,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index b8f1e6b3dd236..c0111afad2893 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -761,6 +761,20 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont return buttonEl.click(); }, + async setTableSummaryRowFunction( + summaryFunction: 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max' + ) { + await testSubjects.click('lnsDatatable_summaryrow_function'); + await testSubjects.click('lns-datatable-summary-' + summaryFunction); + }, + + async setTableSummaryRowLabel(newLabel: string) { + await testSubjects.setValue('lnsDatatable_summaryrow_label', newLabel, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + }, + async setTableDynamicColoring(coloringType: 'none' | 'cell' | 'text') { await testSubjects.click('lnsDatatable_dynamicColoring_groups_' + coloringType); }, @@ -769,6 +783,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lnsDatatable_dynamicColoring_trigger'); }, + async closeTablePalettePanel() { + await testSubjects.click('lns-indexPattern-PalettePanelContainerBack'); + }, + // different picker from the next one async changePaletteTo(paletteName: string) { await testSubjects.click('lnsDatatable_dynamicColoring_palette_picker'); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 31cf17575bdd9..2de5d83714aee 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -98,14 +98,6 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte }); }, - async assertKibanaHomeFileDataVisLinkExists() { - await testSubjects.existOrFail('homeSynopsisLinkml_file_data_visualizer'); - }, - - async assertKibanaHomeFileDataVisLinkNotExists() { - await testSubjects.missingOrFail('homeSynopsisLinkml_file_data_visualizer'); - }, - async assertRadioGroupValue(testSubject: string, expectedValue: string) { const assertRadioGroupValue = await testSubjects.find(testSubject); const input = await assertRadioGroupValue.findByCssSelector(':checked'); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 7fdfbb45269c3..aff1402f5567e 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -57,14 +57,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts index 91a37d0d98cda..8d3aa3c6b6ada 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts @@ -39,16 +39,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.error.expectForbidden(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should not display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.navigateToKibanaHome(); await ml.navigation.openKibanaNav(); await ml.testExecution.logTestStep('should not display the ML nav link'); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index e58e46e985fd9..2e5216d722518 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -58,14 +58,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav();