diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index 8121405e5ae249..370643789c2cde 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -4,7 +4,6 @@ library 'kibana-pipeline-library' kibanaLibrary.load() def TASK_PARAM = params.TASK ?: params.CI_GROUP - // Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke' def JOB_PARTS = TASK_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' @@ -111,6 +110,8 @@ def getWorkerFromParams(isXpack, job, ciGroup) { return kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh') } else if (job == 'apiIntegration') { return kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh') + } else if (job == 'pluginFunctional') { + return kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh') } else { return kibanaPipeline.ossCiGroupProcess(ciGroup) } diff --git a/api_docs/alerting.json b/api_docs/alerting.json index 13a150d0af00da..979f444659c208 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -5,7 +5,42 @@ "functions": [], "interfaces": [], "enums": [], - "misc": [], + "misc": [ + { + "parentPluginId": "alerting", + "id": "def-public.AlertNavigationHandler", + "type": "Type", + "tags": [], + "label": "AlertNavigationHandler", + "description": [ + "\nReturns information that can be used to navigate to a specific page to view the given rule.\n" + ], + "signature": [ + "(alert: Pick<", + { + "pluginId": "alerting", + "scope": "common", + "docId": "kibAlertingPluginApi", + "section": "def-common.Alert", + "text": "Alert" + }, + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">) => string | ", + { + "pluginId": "kibanaUtils", + "scope": "common", + "docId": "kibKibanaUtilsPluginApi", + "section": "def-common.JsonObject", + "text": "JsonObject" + } + ], + "source": { + "path": "x-pack/plugins/alerting/public/alert_navigation_registry/types.ts", + "lineNumber": 20 + }, + "deprecated": false, + "initialIsOpen": false + } + ], "objects": [], "setup": { "parentPluginId": "alerting", @@ -24,44 +59,58 @@ "parentPluginId": "alerting", "id": "def-public.PluginSetupContract.registerNavigation", "type": "Function", - "tags": [], + "tags": [ + "throws" + ], "label": "registerNavigation", - "description": [], + "description": [ + "\nRegister a customized view of the particular rule type. Stack Management provides a generic overview, but a developer can register a\ncustom navigation to provide the user an extra link to a more curated view. The alerting plugin doesn't actually do\nanything with this information, but it can be used by other plugins via the `getNavigation` functionality. Currently\nthe trigger_actions_ui plugin uses it to expose the link from the generic rule view in Stack Management.\n" + ], "signature": [ - "(consumer: string, alertType: string, handler: ", - "AlertNavigationHandler", + "(applicationId: string, ruleType: string, handler: ", + { + "pluginId": "alerting", + "scope": "public", + "docId": "kibAlertingPluginApi", + "section": "def-public.AlertNavigationHandler", + "text": "AlertNavigationHandler" + }, ") => void" ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 15 + "lineNumber": 30 }, "deprecated": false, "returnComment": [], "children": [ { "parentPluginId": "alerting", - "id": "def-public.consumer", + "id": "def-public.applicationId", "type": "string", "tags": [], - "label": "consumer", - "description": [], + "label": "applicationId", + "description": [ + "The application id that the user should be navigated to, to view a particular alert in a custom way." + ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 16 + "lineNumber": 31 }, "deprecated": false }, { "parentPluginId": "alerting", - "id": "def-public.alertType", + "id": "def-public.ruleType", "type": "string", "tags": [], - "label": "alertType", - "description": [], + "label": "ruleType", + "description": [ + "The rule type that has been registered with Alerting.Server.PluginSetupContract.registerType. If\nno such rule with that id exists, a warning is output to the console log. It used to throw an error, but that was temporarily moved\nbecause it was causing flaky test failures with https://github.com/elastic/kibana/issues/59229 and needs to be\ninvestigated more." + ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 17 + "lineNumber": 32 }, "deprecated": false }, @@ -71,7 +120,9 @@ "type": "Function", "tags": [], "label": "handler", - "description": [], + "description": [ + "The navigation handler should return either a relative URL, or a state object. This information can be used,\nin conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details." + ], "signature": [ "(alert: Pick<", { @@ -81,15 +132,7 @@ "section": "def-common.Alert", "text": "Alert" }, - ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">, alertType: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.AlertType", - "text": "AlertType" - }, - "<\"default\", \"recovered\">) => string | ", + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">) => string | ", { "pluginId": "kibanaUtils", "scope": "common", @@ -100,7 +143,7 @@ ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 18 + "lineNumber": 33 }, "deprecated": false } @@ -112,29 +155,39 @@ "type": "Function", "tags": [], "label": "registerDefaultNavigation", - "description": [], + "description": [ + "\nRegister a customized view for all rule types. Stack Management provides a generic overview, but a developer can register a\ncustom navigation to provide the user an extra link to a more curated view. The alerting plugin doesn't actually do\nanything with this information, but it can be used by other plugins via the `getNavigation` functionality. Currently\nthe trigger_actions_ui plugin uses it to expose the link from the generic rule view in Stack Management.\n" + ], "signature": [ - "(consumer: string, handler: ", - "AlertNavigationHandler", + "(applicationId: string, handler: ", + { + "pluginId": "alerting", + "scope": "public", + "docId": "kibAlertingPluginApi", + "section": "def-public.AlertNavigationHandler", + "text": "AlertNavigationHandler" + }, ") => void" ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 20 + "lineNumber": 46 }, "deprecated": false, "returnComment": [], "children": [ { "parentPluginId": "alerting", - "id": "def-public.consumer", + "id": "def-public.applicationId", "type": "string", "tags": [], - "label": "consumer", - "description": [], + "label": "applicationId", + "description": [ + "The application id that the user should be navigated to, to view a particular alert in a custom way." + ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 20 + "lineNumber": 46 }, "deprecated": false }, @@ -144,7 +197,9 @@ "type": "Function", "tags": [], "label": "handler", - "description": [], + "description": [ + "The navigation handler should return either a relative URL, or a state object. This information can be used,\nin conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details." + ], "signature": [ "(alert: Pick<", { @@ -154,15 +209,7 @@ "section": "def-common.Alert", "text": "Alert" }, - ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">, alertType: ", - { - "pluginId": "alerting", - "scope": "common", - "docId": "kibAlertingPluginApi", - "section": "def-common.AlertType", - "text": "AlertType" - }, - "<\"default\", \"recovered\">) => string | ", + ", \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">) => string | ", { "pluginId": "kibanaUtils", "scope": "common", @@ -173,7 +220,7 @@ ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 20 + "lineNumber": 46 }, "deprecated": false } @@ -192,7 +239,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 22 + "lineNumber": 48 }, "deprecated": false, "children": [ @@ -224,7 +271,7 @@ ], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 23 + "lineNumber": 49 }, "deprecated": false, "returnComment": [], @@ -238,7 +285,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/public/plugin.ts", - "lineNumber": 23 + "lineNumber": 49 }, "deprecated": false } @@ -3857,7 +3904,7 @@ "label": "ReservedActionGroups", "description": [], "signature": [ - "\"recovered\" | RecoveryActionGroupId" + "RecoveryActionGroupId | \"recovered\"" ], "source": { "path": "x-pack/plugins/alerting/common/builtin_action_groups.ts", diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 5dce4a9a2c7b17..c3c844148106f0 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -29,6 +29,9 @@ import alertingObj from './alerting.json'; ### Start +### Consts, variables and types + + ## Server ### Functions diff --git a/api_docs/cases.json b/api_docs/cases.json index bc92995dff6e91..cccf5de2710e28 100644 --- a/api_docs/cases.json +++ b/api_docs/cases.json @@ -34,21 +34,9 @@ "text": "CasesUiStart" }, ", ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.SetupPlugins", - "text": "SetupPlugins" - }, + "SetupPlugins", ", ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" - }, + "StartPlugins", ">" ], "source": { @@ -117,13 +105,7 @@ "text": "CoreSetup" }, ", plugins: ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.SetupPlugins", - "text": "SetupPlugins" - }, + "SetupPlugins", ") => void" ], "source": { @@ -164,13 +146,7 @@ "label": "plugins", "description": [], "signature": [ - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.SetupPlugins", - "text": "SetupPlugins" - } + "SetupPlugins" ], "source": { "path": "x-pack/plugins/cases/public/plugin.ts", @@ -199,127 +175,676 @@ "text": "CoreStart" }, ", plugins: ", + "StartPlugins", + ") => ", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CasesUiStart", + "text": "CasesUiStart" + } + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 38 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.CasesUiPlugin.start.$1", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "public", + "docId": "kibCorePluginApi", + "section": "def-public.CoreStart", + "text": "CoreStart" + } + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 38 + }, + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "cases", + "id": "def-public.CasesUiPlugin.start.$2", + "type": "Object", + "tags": [], + "label": "plugins", + "description": [], + "signature": [ + "StartPlugins" + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 38 + }, + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "cases", + "id": "def-public.CasesUiPlugin.stop", + "type": "Function", + "tags": [], + "label": "stop", + "description": [], + "signature": [ + "() => void" + ], + "source": { + "path": "x-pack/plugins/cases/public/plugin.ts", + "lineNumber": 92 + }, + "deprecated": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [ + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps", + "type": "Interface", + "tags": [], + "label": "AllCasesProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 13 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.caseDetailsNavigation", + "type": "Object", + "tags": [], + "label": "caseDetailsNavigation", + "description": [], + "signature": [ + "CasesNavigation", + "<", + "CaseDetailsHrefSchema", + ", \"configurable\">" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 14 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.configureCasesNavigation", + "type": "Object", + "tags": [], + "label": "configureCasesNavigation", + "description": [], + "signature": [ + "CasesNavigation", + ", null>" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 15 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.createCaseNavigation", + "type": "Object", + "tags": [], + "label": "createCaseNavigation", + "description": [], + "signature": [ + "CasesNavigation", + ", null>" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 16 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesProps.userCanCrud", + "type": "boolean", + "tags": [], + "label": "userCanCrud", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/index.tsx", + "lineNumber": 17 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps", + "type": "Interface", + "tags": [], + "label": "AllCasesSelectorModalProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 23 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.alertData", + "type": "Object", + "tags": [], + "label": "alertData", + "description": [], + "signature": [ + "Pick<{ type: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CommentType", + "text": "CommentType" + }, + ".alert | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CommentType", + "text": "CommentType" + }, + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; }, \"index\" | \"rule\" | \"alertId\" | \"owner\"> | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 24 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.createCaseNavigation", + "type": "Object", + "tags": [], + "label": "createCaseNavigation", + "description": [], + "signature": [ + "CasesNavigation", + ", null>" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 25 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.hiddenStatuses", + "type": "Array", + "tags": [], + "label": "hiddenStatuses", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CaseStatusWithAllStatus", + "text": "CaseStatusWithAllStatus" + }, + "[] | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 26 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.onRowClick", + "type": "Function", + "tags": [], + "label": "onRowClick", + "description": [], + "signature": [ + "(theCase?: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + " | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.SubCase", + "text": "SubCase" + }, + " | undefined) => void" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 27 + }, + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.theCase", + "type": "CompoundType", + "tags": [], + "label": "theCase", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + " | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.SubCase", + "text": "SubCase" + }, + " | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 27 + }, + "deprecated": false + } + ] + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.updateCase", + "type": "Function", + "tags": [], + "label": "updateCase", + "description": [], + "signature": [ + "((newCase: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + ") => void) | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 28 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.AllCasesSelectorModalProps.userCanCrud", + "type": "boolean", + "tags": [], + "label": "userCanCrud", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx", + "lineNumber": 29 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CaseViewProps", + "type": "Interface", + "tags": [], + "label": "CaseViewProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + }, + " extends ", + "CaseViewComponentProps" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/case_view/index.tsx", + "lineNumber": 63 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.CaseViewProps.onCaseDataSuccess", + "type": "Function", + "tags": [], + "label": "onCaseDataSuccess", + "description": [], + "signature": [ + "((data: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + ") => void) | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/case_view/index.tsx", + "lineNumber": 64 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CaseViewProps.timelineIntegration", + "type": "Object", + "tags": [], + "label": "timelineIntegration", + "description": [], + "signature": [ + "CasesTimelineIntegration", + " | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/case_view/index.tsx", + "lineNumber": 65 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.ConfigureCasesProps", + "type": "Interface", + "tags": [], + "label": "ConfigureCasesProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/configure_cases/index.tsx", + "lineNumber": 55 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.ConfigureCasesProps.userCanCrud", + "type": "boolean", + "tags": [], + "label": "userCanCrud", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/configure_cases/index.tsx", + "lineNumber": 56 + }, + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps", + "type": "Interface", + "tags": [], + "label": "CreateCaseProps", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + }, + " extends ", + "Owner" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 34 + }, + "deprecated": false, + "children": [ + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.afterCaseCreated", + "type": "Function", + "tags": [], + "label": "afterCaseCreated", + "description": [], + "signature": [ + "((theCase: ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.Case", + "text": "Case" + }, + ", postComment: (args: PostComment) => Promise) => Promise) | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 35 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.caseType", + "type": "CompoundType", + "tags": [], + "label": "caseType", + "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CaseType", + "text": "CaseType" + }, + " | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 36 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.hideConnectorServiceNowSir", + "type": "CompoundType", + "tags": [], + "label": "hideConnectorServiceNowSir", + "description": [], + "signature": [ + "boolean | undefined" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 37 + }, + "deprecated": false + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.onCancel", + "type": "Function", + "tags": [], + "label": "onCancel", + "description": [], + "signature": [ + "() => void" + ], + "source": { + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 38 + }, + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "cases", + "id": "def-public.CreateCaseProps.onSuccess", + "type": "Function", + "tags": [], + "label": "onSuccess", + "description": [], + "signature": [ + "(theCase: ", { "pluginId": "cases", - "scope": "public", + "scope": "common", "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" + "section": "def-common.Case", + "text": "Case" }, - ") => ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.CasesUiStart", - "text": "CasesUiStart" - } + ") => Promise" ], "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 38 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 39 }, "deprecated": false, + "returnComment": [], "children": [ { "parentPluginId": "cases", - "id": "def-public.CasesUiPlugin.start.$1", - "type": "Object", - "tags": [], - "label": "core", - "description": [], - "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.CoreStart", - "text": "CoreStart" - } - ], - "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 38 - }, - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "cases", - "id": "def-public.CasesUiPlugin.start.$2", + "id": "def-public.theCase", "type": "Object", "tags": [], - "label": "plugins", + "label": "theCase", "description": [], "signature": [ { "pluginId": "cases", - "scope": "public", + "scope": "common", "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" + "section": "def-common.Case", + "text": "Case" } ], "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 38 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 39 }, - "deprecated": false, - "isRequired": true + "deprecated": false } - ], - "returnComment": [] + ] }, { "parentPluginId": "cases", - "id": "def-public.CasesUiPlugin.stop", - "type": "Function", + "id": "def-public.CreateCaseProps.timelineIntegration", + "type": "Object", "tags": [], - "label": "stop", + "label": "timelineIntegration", "description": [], "signature": [ - "() => void" + "CasesTimelineIntegration", + " | undefined" ], "source": { - "path": "x-pack/plugins/cases/public/plugin.ts", - "lineNumber": 92 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 40 }, - "deprecated": false, - "children": [], - "returnComment": [] - } - ], - "initialIsOpen": false - } - ], - "functions": [], - "interfaces": [ - { - "parentPluginId": "cases", - "id": "def-public.Owner", - "type": "Interface", - "tags": [], - "label": "Owner", - "description": [], - "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 42 - }, - "deprecated": false, - "children": [ + "deprecated": false + }, { "parentPluginId": "cases", - "id": "def-public.Owner.owner", - "type": "Array", + "id": "def-public.CreateCaseProps.withSteps", + "type": "CompoundType", "tags": [], - "label": "owner", + "label": "withSteps", "description": [], "signature": [ - "string[]" + "boolean | undefined" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 43 + "path": "x-pack/plugins/cases/public/components/create/index.tsx", + "lineNumber": 41 }, "deprecated": false } @@ -328,148 +853,100 @@ }, { "parentPluginId": "cases", - "id": "def-public.SetupPlugins", + "id": "def-public.RecentCasesProps", "type": "Interface", "tags": [], - "label": "SetupPlugins", + "label": "RecentCasesProps", "description": [], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + }, + " extends ", + "Owner" + ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 22 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 20 }, "deprecated": false, "children": [ { "parentPluginId": "cases", - "id": "def-public.SetupPlugins.security", + "id": "def-public.RecentCasesProps.allCasesNavigation", "type": "Object", "tags": [], - "label": "security", + "label": "allCasesNavigation", "description": [], "signature": [ - { - "pluginId": "security", - "scope": "public", - "docId": "kibSecurityPluginApi", - "section": "def-public.SecurityPluginSetup", - "text": "SecurityPluginSetup" - } + "CasesNavigation", + ", null>" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 23 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 21 }, "deprecated": false }, { "parentPluginId": "cases", - "id": "def-public.SetupPlugins.triggersActionsUi", + "id": "def-public.RecentCasesProps.caseDetailsNavigation", "type": "Object", "tags": [], - "label": "triggersActionsUi", + "label": "caseDetailsNavigation", "description": [], "signature": [ - { - "pluginId": "triggersActionsUi", - "scope": "public", - "docId": "kibTriggersActionsUiPluginApi", - "section": "def-public.TriggersAndActionsUIPublicPluginSetup", - "text": "TriggersAndActionsUIPublicPluginSetup" - } + "CasesNavigation", + "<", + "CaseDetailsHrefSchema", + ", \"configurable\">" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 24 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 22 }, "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "cases", - "id": "def-public.StartPlugins", - "type": "Interface", - "tags": [], - "label": "StartPlugins", - "description": [], - "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 27 - }, - "deprecated": false, - "children": [ + }, { "parentPluginId": "cases", - "id": "def-public.StartPlugins.triggersActionsUi", + "id": "def-public.RecentCasesProps.createCaseNavigation", "type": "Object", "tags": [], - "label": "triggersActionsUi", + "label": "createCaseNavigation", "description": [], "signature": [ - { - "pluginId": "triggersActionsUi", - "scope": "public", - "docId": "kibTriggersActionsUiPluginApi", - "section": "def-public.TriggersAndActionsUIPublicPluginStart", - "text": "TriggersAndActionsUIPublicPluginStart" - } + "CasesNavigation", + ", null>" ], "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 28 + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 23 }, "deprecated": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "cases", - "id": "def-public.StartServices", - "type": "Type", - "tags": [], - "label": "StartServices", - "description": [ - "\nTODO: The extra security service is one that should be implemented in the kibana context of the consuming application.\nSecurity is needed for access to authc for the `useCurrentUser` hook. Security_Solution currently passes it via renderApp in public/plugin.tsx\nLeaving it out currently in lieu of RBAC changes" - ], - "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.CoreStart", - "text": "CoreStart" - }, - " & ", - { - "pluginId": "cases", - "scope": "public", - "docId": "kibCasesPluginApi", - "section": "def-public.StartPlugins", - "text": "StartPlugins" }, - " & { security: ", { - "pluginId": "security", - "scope": "public", - "docId": "kibSecurityPluginApi", - "section": "def-public.SecurityPluginSetup", - "text": "SecurityPluginSetup" - }, - "; }" + "parentPluginId": "cases", + "id": "def-public.RecentCasesProps.maxCasesToShow", + "type": "number", + "tags": [], + "label": "maxCasesToShow", + "description": [], + "source": { + "path": "x-pack/plugins/cases/public/components/recent_cases/index.tsx", + "lineNumber": 24 + }, + "deprecated": false + } ], - "source": { - "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 37 - }, - "deprecated": false, "initialIsOpen": false } ], + "enums": [], + "misc": [], "objects": [], "start": { "parentPluginId": "cases", @@ -490,20 +967,36 @@ "type": "Function", "tags": [], "label": "getAllCases", - "description": [], + "description": [ + "\nGet the all cases table" + ], "signature": [ "(props: ", - "AllCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + }, ") => React.ReactElement<", - "AllCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 47 + "lineNumber": 52 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component that displays all cases" + ], "children": [ { "parentPluginId": "cases", @@ -511,13 +1004,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "AllCasesProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesProps", + "text": "AllCasesProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 47 + "lineNumber": 52 }, "deprecated": false } @@ -529,20 +1030,36 @@ "type": "Function", "tags": [], "label": "getAllCasesSelectorModal", - "description": [], + "description": [ + "\nuse Modal hook for all cases selector" + ], "signature": [ "(props: ", - "AllCasesSelectorModalProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + }, ") => React.ReactElement<", - "AllCasesSelectorModalProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 48 + "lineNumber": 58 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component that is a modal for selecting a case" + ], "children": [ { "parentPluginId": "cases", @@ -550,13 +1067,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], + "description": [ + "UseAllCasesSelectorModalProps" + ], "signature": [ - "AllCasesSelectorModalProps" + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.AllCasesSelectorModalProps", + "text": "AllCasesSelectorModalProps" + } ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 49 + "lineNumber": 59 }, "deprecated": false } @@ -568,20 +1093,36 @@ "type": "Function", "tags": [], "label": "getCaseView", - "description": [], + "description": [ + "\nGet the case view component" + ], "signature": [ "(props: ", - "CaseViewProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + }, ") => React.ReactElement<", - "CaseViewProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 51 + "lineNumber": 66 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for viewing a specific case" + ], "children": [ { "parentPluginId": "cases", @@ -589,13 +1130,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "CaseViewProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CaseViewProps", + "text": "CaseViewProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 51 + "lineNumber": 66 }, "deprecated": false } @@ -607,20 +1156,36 @@ "type": "Function", "tags": [], "label": "getConfigureCases", - "description": [], + "description": [ + "\nGet the configure case component" + ], "signature": [ "(props: ", - "ConfigureCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + }, ") => React.ReactElement<", - "ConfigureCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 52 + "lineNumber": 72 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for configuring a specific case" + ], "children": [ { "parentPluginId": "cases", @@ -628,13 +1193,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "ConfigureCasesProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.ConfigureCasesProps", + "text": "ConfigureCasesProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 52 + "lineNumber": 72 }, "deprecated": false } @@ -646,20 +1219,36 @@ "type": "Function", "tags": [], "label": "getCreateCase", - "description": [], + "description": [ + "\nGet the create case form" + ], "signature": [ "(props: ", - "CreateCaseProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + }, ") => React.ReactElement<", - "CreateCaseProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 53 + "lineNumber": 78 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for creating a new case" + ], "children": [ { "parentPluginId": "cases", @@ -667,13 +1256,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "CreateCaseProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.CreateCaseProps", + "text": "CreateCaseProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 53 + "lineNumber": 78 }, "deprecated": false } @@ -685,20 +1282,36 @@ "type": "Function", "tags": [], "label": "getRecentCases", - "description": [], + "description": [ + "\nGet the recent cases component" + ], "signature": [ "(props: ", - "RecentCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + }, ") => React.ReactElement<", - "RecentCasesProps", + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + }, ">" ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 54 + "lineNumber": 84 }, "deprecated": false, - "returnComment": [], + "returnComment": [ + "A react component for showing recent cases" + ], "children": [ { "parentPluginId": "cases", @@ -706,13 +1319,21 @@ "type": "Object", "tags": [], "label": "props", - "description": [], - "signature": [ + "description": [ "RecentCasesProps" ], + "signature": [ + { + "pluginId": "cases", + "scope": "public", + "docId": "kibCasesPluginApi", + "section": "def-public.RecentCasesProps", + "text": "RecentCasesProps" + } + ], "source": { "path": "x-pack/plugins/cases/public/types.ts", - "lineNumber": 54 + "lineNumber": 84 }, "deprecated": false } @@ -6138,7 +6759,7 @@ "label": "Comment", "description": [], "signature": [ - "({ comment: string; type: ", + "({ type: ", { "pluginId": "cases", "scope": "common", @@ -6146,7 +6767,15 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } & { associationType: ", + ".alert | ", + { + "pluginId": "cases", + "scope": "common", + "docId": "kibCasesPluginApi", + "section": "def-common.CommentType", + "text": "CommentType" + }, + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { associationType: ", { "pluginId": "cases", "scope": "common", @@ -6170,15 +6799,7 @@ "section": "def-common.ElasticUser", "text": "ElasticUser" }, - " | null; version: string; }) | ({ type: ", - { - "pluginId": "cases", - "scope": "common", - "docId": "kibCasesPluginApi", - "section": "def-common.CommentType", - "text": "CommentType" - }, - ".alert | ", + " | null; version: string; }) | ({ comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -6186,7 +6807,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { associationType: ", + ".user; owner: string; } & { associationType: ", { "pluginId": "cases", "scope": "common", @@ -6333,7 +6954,7 @@ "label": "CommentPatchRequest", "description": [], "signature": [ - "({ comment: string; type: ", + "({ type: ", { "pluginId": "cases", "scope": "common", @@ -6341,7 +6962,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } & { id: string; version: string; }) | ({ type: ", + ".alert | ", { "pluginId": "cases", "scope": "common", @@ -6349,7 +6970,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".alert | ", + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { id: string; version: string; }) | ({ comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -6357,7 +6978,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } & { id: string; version: string; })" + ".user; owner: string; } & { id: string; version: string; })" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/comment.ts", @@ -6374,7 +6995,7 @@ "label": "CommentRequest", "description": [], "signature": [ - "{ comment: string; type: ", + "{ type: ", { "pluginId": "cases", "scope": "common", @@ -6382,7 +7003,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".user; owner: string; } | { type: ", + ".alert | ", { "pluginId": "cases", "scope": "common", @@ -6390,7 +7011,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".alert | ", + ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; } | { comment: string; type: ", { "pluginId": "cases", "scope": "common", @@ -6398,7 +7019,7 @@ "section": "def-common.CommentType", "text": "CommentType" }, - ".generatedAlert; alertId: string | string[]; index: string | string[]; rule: { id: string | null; name: string | null; }; owner: string; }" + ".user; owner: string; }" ], "source": { "path": "x-pack/plugins/cases/common/api/cases/comment.ts", diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index 0f9cbe5364b630..00714b3217fca8 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -32,9 +32,6 @@ import casesObj from './cases.json'; ### Interfaces -### Consts, variables and types - - ## Server ### Classes diff --git a/api_docs/deprecations.mdx b/api_docs/deprecations.mdx index d9261b943d1708..74dae7faf838a4 100644 --- a/api_docs/deprecations.mdx +++ b/api_docs/deprecations.mdx @@ -111,6 +111,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [clone_panel_action.tsx#L14](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#L14) | - | | | [clone_panel_action.tsx#L98](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#L98) | - | | | [clone_panel_action.tsx#L126](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx#L126) | - | +| | [use_dashboard_state_manager.ts#L23](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts#L23) | - | +| | [use_dashboard_state_manager.ts#L35](https://github.com/elastic/kibana/tree/master/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts#L35) | - | @@ -127,8 +129,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [search_embeddable.ts#L23](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L23) | - | | | [search_embeddable.ts#L59](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L59) | - | | | [kibana_services.ts#L104](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/kibana_services.ts#L104) | - | -| | [search_embeddable.ts#L23](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L23) | - | -| | [search_embeddable.ts#L59](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/embeddable/search_embeddable.ts#L59) | - | | | [kibana_services.ts#L101](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/kibana_services.ts#L101) | - | | | [create_doc_table_react.tsx#L15](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx#L15) | - | | | [create_doc_table_react.tsx#L25](https://github.com/elastic/kibana/tree/master/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx#L25) | - | @@ -153,7 +153,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| | | [attribute_service.tsx#L13](https://github.com/elastic/kibana/tree/master/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#L13) | - | -| | [attribute_service.tsx#L165](https://github.com/elastic/kibana/tree/master/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#L165) | - | +| | [attribute_service.tsx#L167](https://github.com/elastic/kibana/tree/master/src/plugins/embeddable/public/lib/attribute_service/attribute_service.tsx#L167) | - | @@ -189,7 +189,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [query_bar.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx#L30) | - | | | [query_bar.tsx#L38](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/query_bar.tsx#L38) | - | | | [plugin.ts#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/server/plugin.ts#L14) | - | -| | [plugin.ts#L190](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/server/plugin.ts#L190) | - | +| | [plugin.ts#L189](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/server/plugin.ts#L189) | - | | | [plugin.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/target/types/server/plugin.d.ts#L2) | - | | | [plugin.d.ts#L84](https://github.com/elastic/kibana/tree/master/x-pack/plugins/fleet/target/types/server/plugin.d.ts#L84) | - | @@ -228,18 +228,18 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| -| | [plugin.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L12) | 7.16 | -| | [plugin.ts#L38](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L38) | 7.16 | +| | [plugin.ts#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L14) | 7.16 | +| | [plugin.ts#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/plugin.ts#L42) | 7.16 | | | [types.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L9) | 7.16 | -| | [types.ts#L39](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L39) | 7.16 | +| | [types.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L40) | 7.16 | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L1) | 7.16 | -| | [types.d.ts#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L24) | 7.16 | +| | [types.d.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L25) | 7.16 | | | [types.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L10) | 7.16 | -| | [types.ts#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L42) | 7.16 | -| | [types.ts#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L49) | 7.16 | +| | [types.ts#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L43) | 7.16 | +| | [types.ts#L50](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/server/types.ts#L50) | 7.16 | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L1) | 7.16 | -| | [types.d.ts#L26](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L26) | 7.16 | -| | [types.d.ts#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L32) | 7.16 | +| | [types.d.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L27) | 7.16 | +| | [types.d.ts#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/index_management/target/types/server/types.d.ts#L33) | 7.16 | @@ -578,6 +578,11 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [custom_metric_form.d.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.d.ts#L8) | - | | | [index.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/public/pages/metrics/inventory_view/components/waffle/metric_control/index.d.ts#L1) | - | | | [index.d.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/public/pages/metrics/inventory_view/components/waffle/metric_control/index.d.ts#L9) | - | +| | [log_entry_categories_analysis.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts#L9) | 7.16 | +| | [log_entry_categories_analysis.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts#L139) | 7.16 | +| | [log_entry_categories_analysis.ts#L405](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts#L405) | 7.16 | +| | [log_entry_categories_analysis.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/server/lib/log_analysis/log_entry_categories_analysis.d.ts#L1) | 7.16 | +| | [log_entry_categories_analysis.d.ts#L58](https://github.com/elastic/kibana/tree/master/x-pack/plugins/infra/target/types/server/lib/log_analysis/log_entry_categories_analysis.d.ts#L58) | 7.16 | @@ -587,8 +592,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | ---------------|-----------|-----------| | | [embeddable.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L14) | - | | | [embeddable.tsx#L85](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L85) | - | -| | [index.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L25) | - | -| | [index.tsx#L102](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L102) | - | | | [field_item.tsx#L47](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L47) | - | | | [field_item.tsx#L172](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L172) | - | | | [datapanel.tsx#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx#L42) | - | @@ -603,10 +606,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L8) | - | | | [types.ts#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L57) | - | | | [field_stats.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L11) | - | -| | [field_stats.ts#L141](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L141) | - | -| | [field_stats.ts#L250](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L250) | - | -| | [field_stats.ts#L290](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L290) | - | -| | [field_stats.ts#L332](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L332) | - | +| | [field_stats.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L139) | - | +| | [field_stats.ts#L248](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L248) | - | +| | [field_stats.ts#L287](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L287) | - | +| | [field_stats.ts#L329](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L329) | - | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L1) | - | | | [types.d.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L22) | - | | | [field_stats.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L3) | - | @@ -618,10 +621,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L8) | - | | | [types.ts#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L57) | - | | | [field_stats.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L11) | - | -| | [field_stats.ts#L141](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L141) | - | -| | [field_stats.ts#L250](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L250) | - | -| | [field_stats.ts#L290](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L290) | - | -| | [field_stats.ts#L332](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L332) | - | +| | [field_stats.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L139) | - | +| | [field_stats.ts#L248](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L248) | - | +| | [field_stats.ts#L287](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L287) | - | +| | [field_stats.ts#L329](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L329) | - | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L1) | - | | | [types.d.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L22) | - | | | [field_stats.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L3) | - | @@ -630,8 +633,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [field_stats.d.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L9) | - | | | [embeddable.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L14) | - | | | [embeddable.tsx#L85](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx#L85) | - | -| | [index.tsx#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L25) | - | -| | [index.tsx#L102](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx#L102) | - | | | [field_item.tsx#L47](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L47) | - | | | [field_item.tsx#L172](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx#L172) | - | | | [datapanel.tsx#L42](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx#L42) | - | @@ -646,10 +647,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L8) | - | | | [types.ts#L57](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/public/indexpattern_datasource/types.ts#L57) | - | | | [field_stats.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L11) | - | -| | [field_stats.ts#L141](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L141) | - | -| | [field_stats.ts#L250](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L250) | - | -| | [field_stats.ts#L290](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L290) | - | -| | [field_stats.ts#L332](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L332) | - | +| | [field_stats.ts#L139](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L139) | - | +| | [field_stats.ts#L248](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L248) | - | +| | [field_stats.ts#L287](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L287) | - | +| | [field_stats.ts#L329](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/server/routes/field_stats.ts#L329) | - | | | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L1) | - | | | [types.d.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts#L22) | - | | | [field_stats.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts#L3) | - | @@ -935,7 +936,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L8) | - | -| | [types.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L44) | - | +| | [types.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L45) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | | | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | @@ -1125,7 +1126,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [get_docvalue_source_fields.test.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L10) | - | | | [get_docvalue_source_fields.test.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_search_source/get_docvalue_source_fields.test.ts#L12) | - | | | [types.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L8) | - | -| | [types.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L44) | - | +| | [types.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/embeddable/types.ts#L45) | - | | | [es_doc_field.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L12) | - | | | [es_doc_field.ts#L45](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts#L45) | - | | | [es_source.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts#L10) | - | @@ -1360,7 +1361,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [static_globals.ts#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/static_globals.ts#L43) | 7.16 | | | [plugin.ts#L18](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L18) | 7.16 | | | [plugin.ts#L74](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L74) | 7.16 | -| | [plugin.ts#L284](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L284) | 7.16 | +| | [plugin.ts#L279](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/server/plugin.ts#L279) | 7.16 | | | [types.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/target/types/server/types.d.ts#L2) | 7.16 | | | [types.d.ts#L47](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/target/types/server/types.d.ts#L47) | 7.16 | | | [plugin.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/monitoring/target/types/server/plugin.d.ts#L1) | 7.16 | @@ -1449,22 +1450,22 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| -| | [types.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L19) | - | -| | [types.ts#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L104) | - | +| | [types.ts#L18](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L18) | - | +| | [types.ts#L95](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L95) | - | | | [utils.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L10) | - | | | [utils.ts#L53](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L53) | - | | | [utils.ts#L61](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L61) | - | | | [utils.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L69) | - | | | [default_configs.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L19) | - | -| | [default_configs.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L25) | - | -| | [types.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L19) | - | -| | [types.ts#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L104) | - | +| | [default_configs.ts#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L24) | - | +| | [types.ts#L18](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L18) | - | +| | [types.ts#L95](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts#L95) | - | | | [utils.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L10) | - | | | [utils.ts#L53](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L53) | - | | | [utils.ts#L61](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L61) | - | | | [utils.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#L69) | - | | | [default_configs.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L19) | - | -| | [default_configs.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L25) | - | +| | [default_configs.ts#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#L24) | - | @@ -1540,9 +1541,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| -| | [types.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L22) | - | -| | [types.ts#L72](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L72) | - | -| | [action.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L20) | - | +| | [types.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L21) | - | +| | [types.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L66) | - | +| | [action.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L19) | - | | | [action.ts#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L100) | - | | | [index.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L8) | - | | | [index.ts#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L86) | - | @@ -1579,9 +1580,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L48](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L48) | - | -| | [middleware.ts#L64](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L64) | - | -| | [middleware.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L69) | - | +| | [middleware.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L44) | - | +| | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | +| | [middleware.ts#L65](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L65) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1636,8 +1637,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L31) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L30) | - | -| | [query.ts#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L13) | - | -| | [query.ts#L52](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L52) | - | | | [model.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L8) | - | | | [model.ts#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L30) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L33) | - | @@ -1754,9 +1753,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.tsx#L242](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx#L242) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx#L12) | - | | | [index.tsx#L35](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx#L35) | - | -| | [types.ts#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L22) | - | -| | [types.ts#L72](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L72) | - | -| | [action.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L20) | - | +| | [types.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L21) | - | +| | [types.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L66) | - | +| | [action.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L19) | - | | | [action.ts#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L100) | - | | | [index.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L8) | - | | | [index.ts#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L86) | - | @@ -1793,9 +1792,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L48](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L48) | - | -| | [middleware.ts#L64](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L64) | - | -| | [middleware.ts#L69](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L69) | - | +| | [middleware.ts#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L44) | - | +| | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | +| | [middleware.ts#L65](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L65) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1850,8 +1849,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L31) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L30) | - | -| | [query.ts#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L13) | - | -| | [query.ts#L52](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/query.ts#L52) | - | | | [model.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L8) | - | | | [model.ts#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L30) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L33) | - | diff --git a/dev_docs/tutorials/building_a_plugin.mdx b/dev_docs/tutorials/building_a_plugin.mdx index cee5a9a399de5b..e751ce7d01b165 100644 --- a/dev_docs/tutorials/building_a_plugin.mdx +++ b/dev_docs/tutorials/building_a_plugin.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/tutorials/build-a-plugin title: Kibana plugin tutorial summary: Anatomy of a Kibana plugin and how to build one date: 2021-02-05 -tags: ['kibana','onboarding', 'dev', 'tutorials'] +tags: ['kibana', 'onboarding', 'dev', 'tutorials'] --- Prereading material: @@ -14,7 +14,7 @@ Prereading material: ## The anatomy of a plugin Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, -or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, +or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, and you interact with Core and other plugins in the same way. The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be: @@ -33,7 +33,7 @@ plugins/ index.ts [6] ``` -### [1] kibana.json +### [1] kibana.json `kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both: @@ -42,14 +42,33 @@ plugins/ "id": "demo", "version": "kibana", "server": true, - "ui": true + "ui": true, + "owner": { [1] + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "This plugin extends Kibana by doing xyz, and allows other plugins to extend Kibana by offering abc functionality. It also exposes some helper utilities that do efg", [2] + "requiredPlugins": ["data"], [3] + "optionalPlugins": ["alerting"] [4] + "requiredBundles": ["anotherPlugin"] [5] } ``` +[1], [2]: Every internal plugin should fill in the owner and description properties. + +[3], [4]: Any plugin that you have a dependency on should be listed in `requiredPlugins` or `optionalPlugins`. Doing this will ensure that you have access to that plugin's start and setup contract inside your own plugin's start and setup lifecycle methods. If a plugin you optionally depend on is not installed or disabled, it will be undefined if you try to access it. If a plugin you require is not installed or disabled, kibana will fail to build. + +[5]: Don't worry too much about getting 5 right. The build optimizer will complain if any of these values are incorrect. + + + + You don't need to declare a dependency on a plugin if you only wish to access its types. + + ### [2] public/index.ts -`public/index.ts` is the entry point into the client-side code of this plugin. It must export a function named plugin, which will receive a standard set of - core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. +`public/index.ts` is the entry point into the client-side code of this plugin. Everything exported from this file will be a part of the plugins . If the plugin only exists to export static utilities, consider using a package. Otherwise, this file must export a function named plugin, which will receive a standard set of +core capabilities as an argument. It should return an instance of its plugin class for Kibana to load. ``` import type { PluginInitializerContext } from 'kibana/server'; @@ -60,13 +79,32 @@ export function plugin(initializerContext: PluginInitializerContext) { } ``` + + +1. When possible, use + +``` +export type { AType } from '...'` +``` + +instead of + +``` +export { AType } from '...'`. +``` + +Using the non-`type` variation will increase the bundle size unnecessarily and may unwillingly provide access to the implementation of `AType` class. + +2. Don't use `export *` in these top level index.ts files + + + ### [3] public/plugin.ts `public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry - point, but all plugins at Elastic should be consistent in this way. +point, but all plugins at Elastic should be consistent in this way. - - ```ts +```ts import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; export class DemoPlugin implements Plugin { @@ -84,10 +122,9 @@ export class DemoPlugin implements Plugin { // called when plugin is torn down during Kibana's shutdown sequence } } - ``` - +``` -### [4] server/index.ts +### [4] server/index.ts `server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: @@ -115,7 +152,7 @@ export class DemoPlugin implements Plugin { } ``` -Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain +Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built. ### [6] common/index.ts @@ -124,8 +161,8 @@ considerations related to how plugins integrate with core APIs and APIs exposed ## How plugin's interact with each other, and Core -The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. -For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, +The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. +For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler, a plugin just accesses it off of the first argument: ```ts @@ -135,14 +172,16 @@ export class DemoPlugin { public setup(core: CoreSetup) { const router = core.http.createRouter(); // handler is called when '/path' resource is requested with `GET` method - router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + router.get({ path: '/path', validate: false }, (context, req, res) => + res.ok({ content: 'ok' }) + ); } } ``` Unlike core, capabilities exposed by plugins are not automatically injected into all plugins. Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a - dependency in it’s kibana.json manifest file. +dependency in it’s kibana.json manifest file. ** foobar plugin.ts: ** @@ -174,8 +213,8 @@ export class MyPlugin implements Plugin { } } ``` -[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. +[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins. ** demo kibana.json** @@ -194,7 +233,7 @@ With that specified in the plugin manifest, the appropriate interfaces are then import type { CoreSetup, CoreStart } from 'kibana/server'; import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; -interface DemoSetupPlugins { [1] +interface DemoSetupPlugins { [1] foobar: FoobarPluginSetup; } @@ -218,7 +257,7 @@ export class DemoPlugin { public stop() {} } ``` - + [1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. [2] These manually constructed types should then be used to specify the type of the second argument to the plugin. diff --git a/docs/api/alerting/list_rule_types.asciidoc b/docs/api/alerting/list_rule_types.asciidoc index 98016c8cf82fa9..31c8416e750593 100644 --- a/docs/api/alerting/list_rule_types.asciidoc +++ b/docs/api/alerting/list_rule_types.asciidoc @@ -8,7 +8,7 @@ Retrieve a list of alerting rule types that the user is authorized to access. Each rule type includes a list of consumer features. Within these features, users are authorized to perform either `read` or `all` operations on rules of that type. This helps determine which rule types users can read, but not create or modify. -NOTE: Some rule types are limited to specific features. These rule types are not available when <> in <>. +NOTE: Some rule types are limited to specific features. These rule types are not available when <> in <>. [[list-rule-types-api-request]] ==== Request diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc index b4afc2788895ce..3e3e2b178ff10b 100644 --- a/docs/apm/apm-alerts.asciidoc +++ b/docs/apm/apm-alerts.asciidoc @@ -15,7 +15,7 @@ and enables central management of all alerts from <>. +see Kibana's <>. The APM app supports four different types of alerts: @@ -126,4 +126,4 @@ See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more in NOTE: If you are using an **on-premise** Elastic Stack deployment with security, communication between Elasticsearch and Kibana must have TLS configured. -More information is in the alerting {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[prerequisites]. \ No newline at end of file +More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites]. \ No newline at end of file diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 67210c9d77057e..272cd524c2c200 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -2893,6 +2893,33 @@ Alias: `type` [[u_fns]] == U +[float] +[[uiSetting_fn]] +=== `uiSetting` + +Returns a UI settings parameter value. + +*Accepts:* `null` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|_Unnamed_ *** + +Aliases: `parameter` +|`string` +|The parameter name. + +|`default` +|`any` +|A default value in case of the parameter is not set. + +Default: `null` +|=== + +*Returns:* `ui_setting` + [float] [[urlparam_fn]] === `urlparam` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md index 449cc66cb3335f..34de4f9e13cda4 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md @@ -23,7 +23,7 @@ export interface ExpressionFunctionDefinitionstring | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | List of allowed type names for input value of this function. If this property is set the input of function will be cast to the first possible type in this list. If this property is missing the input will be provided to the function as-is. | | [name](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | -| [type](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md) | TypeToString<UnwrapPromiseOrReturn<Output>> | Name of type of value this function outputs. | +| [type](./kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md) | TypeString<Output> | UnmappedTypeStrings | Name of type of value this function outputs. | ## Methods diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md index 4831f24a418bc6..01ad35b8a1ba5c 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.type.md @@ -9,5 +9,5 @@ Name of type of value this function outputs. Signature: ```typescript -type?: TypeToString>; +type?: TypeString | UnmappedTypeStrings; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md index 6ba0f0feb82b3a..9afd603bc48694 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.md @@ -70,7 +70,7 @@ The actual function is defined in the fn key. The function can be \ | Method | Modifiers | Description | | --- | --- | --- | -| [setup()](./kibana-plugin-plugins-expressions-public.expressionsservice.setup.md) | | Returns Kibana Platform \*setup\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | -| [start()](./kibana-plugin-plugins-expressions-public.expressionsservice.start.md) | | Returns Kibana Platform \*start\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | +| [setup(args)](./kibana-plugin-plugins-expressions-public.expressionsservice.setup.md) | | Returns Kibana Platform \*setup\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | +| [start(args)](./kibana-plugin-plugins-expressions-public.expressionsservice.start.md) | | Returns Kibana Platform \*start\* life-cycle contract. Useful to return the same contract on server-side and browser-side. | | [stop()](./kibana-plugin-plugins-expressions-public.expressionsservice.stop.md) | | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md index a51f3f073d5181..991f1f717d5f56 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.setup.md @@ -9,8 +9,15 @@ Returns Kibana Platform \*setup\* life-cycle contract. Useful to return the same Signature: ```typescript -setup(): ExpressionsServiceSetup; +setup(...args: unknown[]): ExpressionsServiceSetup; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| args | unknown[] | | + Returns: `ExpressionsServiceSetup` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md index 766d703a0729da..34d33cacabebb6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservice.start.md @@ -9,8 +9,15 @@ Returns Kibana Platform \*start\* life-cycle contract. Useful to return the same Signature: ```typescript -start(): ExpressionsServiceStart; +start(...args: unknown[]): ExpressionsServiceStart; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| args | unknown[] | | + Returns: `ExpressionsServiceStart` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md index 51240f094b181a..35248c01a4e29d 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.md @@ -23,7 +23,7 @@ export interface ExpressionFunctionDefinitionstring | Help text displayed in the Expression editor. This text should be internationalized. | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.inputtypes.md) | Array<TypeToString<Input>> | List of allowed type names for input value of this function. If this property is set the input of function will be cast to the first possible type in this list. If this property is missing the input will be provided to the function as-is. | | [name](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.name.md) | Name | The name of the function, as will be used in expression. | -| [type](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md) | TypeToString<UnwrapPromiseOrReturn<Output>> | Name of type of value this function outputs. | +| [type](./kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md) | TypeString<Output> | UnmappedTypeStrings | Name of type of value this function outputs. | ## Methods diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md index a73ded342f053d..2994b9547fd8c0 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunctiondefinition.type.md @@ -9,5 +9,5 @@ Name of type of value this function outputs. Signature: ```typescript -type?: TypeToString>; +type?: TypeString | UnmappedTypeStrings; ``` diff --git a/docs/management/connectors/action-types/index.asciidoc b/docs/management/connectors/action-types/index.asciidoc index d3bd3d431748c9..7868085ef9c96f 100644 --- a/docs/management/connectors/action-types/index.asciidoc +++ b/docs/management/connectors/action-types/index.asciidoc @@ -119,7 +119,7 @@ When creating a new rule, add an <> and select [role="screenshot"] image::images/pre-configured-alert-history-connector.png[Select pre-configured alert history connectors] -Documents are indexed using a preconfigured schema that captures the <> available for the rule. By default, these documents are indexed into the `kibana-alert-history-default` index, but you can specify a different index. Index names must start with `kibana-alert-history-` to take advantage of the preconfigured alert history index template. +Documents are indexed using a preconfigured schema that captures the <> available for the rule. By default, these documents are indexed into the `kibana-alert-history-default` index, but you can specify a different index. Index names must start with `kibana-alert-history-` to take advantage of the preconfigured alert history index template. [IMPORTANT] ============================================== diff --git a/docs/management/connectors/action-types/webhook.asciidoc b/docs/management/connectors/action-types/webhook.asciidoc index aa52e8a3bdb433..02c3de139e0d5d 100644 --- a/docs/management/connectors/action-types/webhook.asciidoc +++ b/docs/management/connectors/action-types/webhook.asciidoc @@ -91,4 +91,4 @@ Body:: A JSON payload sent to the request URL. For example: Mustache template variables (the text enclosed in double braces, for example, `context.rule.name`) have their values escaped, so that the final JSON will be valid (escaping double quote characters). -For more information on Mustache template variables, refer to <>. +For more information on Mustache template variables, refer to <>. diff --git a/docs/rule-type-template.asciidoc b/docs/rule-type-template.asciidoc index 605bdd57c14920..5fe4de81bcddc1 100644 --- a/docs/rule-type-template.asciidoc +++ b/docs/rule-type-template.asciidoc @@ -6,7 +6,7 @@ Include a short description of the rule type. [float] ==== Create the rule -Fill in the <>, then select **. +Fill in the <>, then select **. [float] ==== Define the conditions @@ -25,7 +25,7 @@ Condition2:: This is another condition the user must define. [float] ==== Add action variables -<> to run when the rule condition is met. The following variables are specific to the rule. You can also specify <>. +<> to run when the rule condition is met. The following variables are specific to the rule. You can also specify <>. `context.variableA`:: A short description of the context variable defined by the rule type. `context.variableB`:: A short description of the context variable defined by the rule type with an example. Example: `this is what variableB outputs`. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index bb11d2a0be4233..8c17f8ec93b965 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -11,7 +11,7 @@ image::images/alerting-overview.png[Rules and Connectors UI] [IMPORTANT] ============================================== -To make sure you can access alerting and actions, see the <> section. +To make sure you can access alerting and actions, see the <> section. ============================================== [float] @@ -22,7 +22,7 @@ Actions typically involve interaction with {kib} services or third party integra This section describes all of these elements and how they operate together. [float] -=== What is a rule? +=== Rules A rule specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: @@ -30,7 +30,10 @@ A rule specifies a background task that runs on the {kib} server to check for sp * *Schedule*: when/how often should detection checks run? * *Actions*: what happens when a condition is detected? -For example, when monitoring a set of servers, a rule might check for average CPU usage > 0.9 on each server for the last two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). +For example, when monitoring a set of servers, a rule might: +* Check for average CPU usage > 0.9 on each server for the last two minutes (condition). +* Check every minute (schedule). +* Send a warning email message via SMTP with subject `CPU on {{server}} is high` (action). image::images/what-is-a-rule.svg[Three components of a rule] @@ -40,7 +43,7 @@ The following sections describe each part of the rule in more detail. [[alerting-concepts-conditions]] ==== Conditions -Under the hood, {kib} rules detect conditions by running a javascript function on the {kib} server, which gives it the flexibility to support a wide range of conditions, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. +Under the hood, {kib} rules detect conditions by running a Javascript function on the {kib} server, which gives it the flexibility to support a wide range of conditions, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. These conditions are packaged and exposed as *rule types*. A rule type hides the underlying details of the condition, and exposes a set of parameters to control the details of the conditions to detect. @@ -68,22 +71,22 @@ Actions are invocations of connectors, which allow interaction with {kib} servic When defining actions in a rule, you specify: -* the *connector type*: the type of service or integration to use -* the connection for that type by referencing a <> -* a mapping of rule values to properties exposed for that type of action +* The *connector type*: the type of service or integration to use +* The connection for that type by referencing a <> +* A mapping of rule values to properties exposed for that type of action The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the rule condition is detected. In the server monitoring example, the `email` connector type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. -When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` connector type. +When the rule detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` connector type. image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] See <> for details on the types of connectors provided by {kib}. [float] -[[alerting-concepts-alert-instances]] +[[alerting-concepts-alerts]] === Alerts When checking for a condition, a rule might identify multiple occurrences of the condition. {kib} tracks each of these *alerts* separately and takes an action per alert. @@ -92,22 +95,6 @@ Using the server monitoring example, each server with average CPU > 0.9 is track image::images/alerts.svg[{kib} tracks each detected condition as an alert and takes action on each alert] -[float] -[[alerting-concepts-suppressing-duplicate-notifications]] -=== Suppressing duplicate notifications - -Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. - -In the above example, three emails are sent for server X123 in the span of 3 minutes for the same rule. Often it's desirable to suppress frequent re-notification. Operations like muting and throttling can be applied at the alert level. If we set the rule re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: - -* Minute 1: server X123 > 0.9. *One email* is sent for server X123. -* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456. -* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. - [float] [[alerting-concepts-connectors]] === Connectors @@ -120,7 +107,7 @@ Rather than repeatedly entering connection information and credentials for each image::images/rule-concepts-connectors.svg[Connectors provide a central place to store service connection settings] [float] -=== Summary +== Putting it all together A *rule* consists of conditions, *actions*, and a schedule. When conditions are met, *alerts* are created that render *actions* and invoke them. To make action setup and update easier, actions use *connectors* that centralize the information used to connect with {kib} services and third-party integrations. The following example ties these concepts together: @@ -131,7 +118,6 @@ image::images/rule-concepts-summary.svg[Rules, connectors, alerts and actions wo . {kib} invokes the actions, sending them to a third party *integration* like an email service. . If the third party integration has connection parameters or credentials, {kib} will fetch these from the *connector* referenced in the action. - [float] [[alerting-concepts-differences]] == Differences from Watcher @@ -152,63 +138,7 @@ Pre-packaged *rule types* simplify setup and hide the details of complex, domain [float] [[alerting-setup-prerequisites]] -== Setup and prerequisites - -If you are using an *on-premises* Elastic Stack deployment: - -* In the kibana.yml configuration file, add the <> setting. -* For emails to have a footer with a link back to {kib}, set the <> configuration setting. - -If you are using an *on-premises* Elastic Stack deployment with <>: - -* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background rule checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. - -[float] -[[alerting-setup-production]] -== Production considerations and scaling guidance - -When relying on alerting and actions as mission critical services, make sure you follow the <>. - -See <> for more information on the scalability of {kib} alerting. - -[float] -[[alerting-security]] -== Security +== Prerequisites +<> -To access alerting in a space, a user must have access to one of the following features: - -* Alerting -* <> -* <> -* <> -* <> -* <> -* <> - -See <> for more information on configuring roles that provide access to these features. -Also note that a user will need +read+ privileges for the *Actions and Connectors* feature to attach actions to a rule or to edit a rule that has an action attached to it. - -[float] -[[alerting-spaces]] -=== Space isolation - -Rules and connectors are isolated to the {kib} space in which they were created. A rule or connector created in one space will not be visible in another. - -[float] -[[alerting-authorization]] -=== Authorization - -Rules, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the rule. Upon creating or modifying a rule, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the rule including detection checks and executing actions. - -[IMPORTANT] -============================================== -If a rule requires certain privileges to run, such as index privileges, keep in mind that if a user without those privileges updates the rule, the rule will no longer function. -============================================== - -[float] -[[alerting-restricting-actions]] -=== Restricting actions - -For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and allowlist the hostnames that {kib} can connect with. - --- +-- \ No newline at end of file diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc new file mode 100644 index 00000000000000..39f1af0030e0aa --- /dev/null +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -0,0 +1,68 @@ +[role="xpack"] +[[alerting-setup]] +== Alerting Setup +++++ +Setup +++++ + +The Alerting feature is automatically enabled in {kib}, but might require some additional configuration. + +[float] +[[alerting-prerequisites]] +=== Prerequisites +If you are using an *on-premises* Elastic Stack deployment: + +* In the kibana.yml configuration file, add the <> setting. +* For emails to have a footer with a link back to {kib}, set the <> configuration setting. + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* You must enable Transport Layer Security (TLS) for communication <>. {kib} alerting uses <> to secure background rule checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. A proxy will not suffice. + +[float] +[[alerting-setup-production]] +=== Production considerations and scaling guidance + +When relying on alerting and actions as mission critical services, make sure you follow the <>. + +See <> for more information on the scalability of {kib} alerting. + +[float] +[[alerting-security]] +=== Security + +To access alerting in a space, a user must have access to one of the following features: + +* Alerting +* <> +* <> +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. +Also note that a user will need +read+ privileges for the *Actions and Connectors* feature to attach actions to a rule or to edit a rule that has an action attached to it. + +[float] +[[alerting-restricting-actions]] +==== Restrict actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and allowlist the hostnames that {kib} can connect with. + +[float] +[[alerting-spaces]] +=== Space isolation + +Rules and connectors are isolated to the {kib} space in which they were created. A rule or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Rules, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the rule. Upon creating or modifying a rule, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the rule including detection checks and executing actions. + +[IMPORTANT] +============================================== +If a rule requires certain privileges to run, such as index privileges, keep in mind that if a user without those privileges updates the rule, the rule will no longer function. +============================================== diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index b7fd98d1c674ec..b7b0c749dfe149 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[alerting-troubleshooting]] == Alerting Troubleshooting +++++ +Troubleshooting +++++ This page describes how to resolve common problems you might encounter with Alerting. If your problem isn’t described here, please review open issues in the following GitHub repositories: diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc new file mode 100644 index 00000000000000..af6714aef662ff --- /dev/null +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -0,0 +1,184 @@ +[role="xpack"] +[[create-and-manage-rules]] +== Create and manage rules + +The *Rules* UI provides a cross-app view of alerting. Different {kib} apps like {observability-guide}/create-alerts.html[*Observability*], {security-guide}/prebuilt-rules.html[*Security*], <> and <> can offer their own rules. The *Rules* UI provides a central place to: + +* <> rules +* <> including enabling/disabling, muting/unmuting, and deleting +* Drill-down to <> + +[role="screenshot"] +image:images/rules-and-connectors-ui.png[Example rule listing in the Rules and Connectors UI] + +For more information on alerting concepts and the types of rules and connectors available, see <>. + +[float] +=== Required permissions + +Access to rules is granted based on your privileges to alerting-enabled features. See <> for more information. + +[float] +[[create-edit-rules]] +=== Create and edit rules + +Many rules must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic rule types can be created in the *Rules* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting a rule type and configuring its conditions and action type. Refer to <> for details on what types of rules are available and how to configure them. + +After a rule is created, you can re-open the flyout and change a rule's properties by clicking the *Edit* button shown on each row of the rule listing. + +[float] +[[defining-rules-general-details]] +==== General rule details + +All rules share the following four properties. + +Name:: The name of the rule. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable rule listing in the *Management* UI. A distinctive name can help identify and find a rule. +Tags:: A list of tag names that can be applied to a rule. Tags can help you organize and find rules, because tags appear in the rule listing in the *Management* UI, which is searchable by tag. +Check every:: This value determines how frequently the rule conditions are checked. Note that the timing of background rule checks is not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. +Notify:: This value limits how often actions are repeated when an alert remains active across rule checks. See <> for more information. + +- **Only on status change**: Actions are not repeated when an alert remains active across checks. Actions run only when the alert status changes. +- **Every time alert is active**: Actions are repeated when an alert remains active across checks. +- **On a custom action interval**: Actions are suppressed for the throttle interval, but repeat when an alert remains active across checks for a duration longer than the throttle interval. + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +[NOTE] +============================================== +Since actions are executed per alert, a rule can end up generating a large number of actions. Take the following example where a rule is monitoring three servers every minute for CPU usage > 0.9, and the rule is set to notify **Every time alert is active**: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, one for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same rule. Often, it's desirable to suppress these re-notifications. If you set the rule **Notify** setting to **On a custom action interval** with an interval of 5 minutes, you reduce noise by only getting emails every 5 minutes for servers that continue to exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +To get notified **only once** when a server exceeds the threshold, you can set the rule's **Notify** setting to **Only on status change**. +============================================== + +[role="screenshot"] +image::images/rule-flyout-general-details.png[alt='All rules have name, tags, check every, and notify properties in common'] + +[float] +[[defining-rules-type-conditions]] +==== Rule type and conditions + +Depending upon the {kib} app and context, you might be prompted to choose the type of rule to create. Some apps will pre-select the type of rule for you. + +[role="screenshot"] +image::images/rule-flyout-rule-type-selection.png[Choosing the type of rule to create] + +Each rule type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold rule, the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. + +[role="screenshot"] +image::images/rule-flyout-rule-conditions.png[UI for defining rule conditions on an index threshold rule] + +[float] +[[defining-rules-actions-details]] +==== Action type and details + +To receive notifications when a rule meets the defined conditions, you must add one or more actions. Start by selecting a type of connector for your action: + +[role="screenshot"] +image::images/rule-flyout-connector-type-selection.png[UI for selecting an action type] + +Each action must specify a <> instance. If no connectors exist for the selected type, click **Add connector** to create one. + +[role="screenshot"] +image::images/rule-flyout-action-no-connector.png[UI for adding connector] + +Once you have selected a connector, use the **Run When** dropdown to choose the action group to associate with this action. When a rule meets the defined condition, it is marked as **Active** and alerts are created and assigned to an action group. In addition to the action groups defined by the selected rule type, each rule also has a **Recovered** action group that is assigned when a rule's conditions are no longer detected. + +Each action type exposes different properties. For example, an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. + +[role="screenshot"] +image::images/rule-flyout-action-details.png[UI for defining an email action] + +[float] +[[defining-rules-actions-variables]] +===== Action variables +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass rule values at the time a condition is detected to an action. You can access the list of available variables using the "add variable" button. Although available variables differ by rule type, all rule types pass the following variables: + +`rule.id`:: The ID of the rule. +`rule.name`:: The name of the rule. +`rule.spaceId`:: The ID of the space for the rule. +`rule.tags`:: The list of tags applied to the rule. +`date`:: The date the rule scheduled the action, in ISO format. +`alert.id`:: The ID of the alert that scheduled the action. +`alert.actionGroup`:: The ID of the action group of the alert that scheduled the action. +`alert.actionSubgroup`:: The action subgroup of the alert that scheduled the action. +`alert.actionGroupName`:: The name of the action group of the alert that scheduled the action. +`kibanaBaseUrl`:: The configured <>. If not configured, this will be empty. + +[role="screenshot"] +image::images/rule-flyout-action-variables.png[Passing rule values to an action] + +Some cases exist where the variable values will be "escaped", when used in a context where escaping is needed: + +- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Markdown. +- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Slack Markdown. +- For the <> connector, the `body` action configuration property escapes any characters that are invalid in JSON string values. + +Mustache also supports "triple braces" of the form `{{{variable name}}}`, which indicates no escaping should be done at all. Care should be used when using this form, as it could end up rendering the variable content in such a way as to make the resulting parameter invalid or formatted incorrectly. + +Each rule type defines additional variables as properties of the variable `context`. For example, if a rule type defines a variable `value`, it can be used in an action parameter as `{{context.value}}`. + +For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the rule type. + +You can attach more than one action. Clicking the "Add action" button will prompt you to select another rule type and repeat the above steps again. + +[role="screenshot"] +image::images/rule-flyout-add-action.png[You can add multiple actions on a rule] + +[NOTE] +============================================== +Actions are not required on rules. You can run a rule without actions to understand its behavior, and then <> later. +============================================== + +[float] +[[controlling-rules]] +=== Mute and disable rules + +The rule listing allows you to quickly mute/unmute, disable/enable, and delete individual rules by clicking menu button to open the actions menu. Muting means that the rule checks continue to run on a schedule, but that alert will not trigger any action. + +[role="screenshot"] +image:images/individual-mute-disable.png[The actions button allows an individual rule to be muted, disabled, or deleted] + +You can perform these operations in bulk by multi-selecting rules, and then clicking the *Manage rules* button: + +[role="screenshot"] +image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk,width=75%] + +[float] +[[rule-details]] +=== Drilldown to rule details + +Select a rule name from the rule listing to access the *Rule details* page, which tells you about the state of the rule and provides granular control over the actions it is taking. + +[role="screenshot"] +image::images/rule-details-alerts-active.png[Rule details page with three alerts] + +In this example, the rule detects when a site serves more than a threshold number of bytes in a 24 hour period. Three sites are above the threshold. These are called alerts - occurrences of the condition being detected - and the alert name, status, time of detection, and duration of the condition are shown in this view. + +Upon detection, each alert can trigger one or more actions. If the condition persists, the same actions will trigger either on the next scheduled rule check, or (if defined) after the re-notify period on the rule has passed. To prevent re-notification, you can suppress future actions by clicking on the switch to mute an individual alert. + +[role="screenshot"] +image::images/rule-details-alert-muting.png[Muting an alert,width=50%] + +Alerts will come and go from the list depending on whether they meet the rule conditions or not - unless they are muted. If a muted instance no longer meets the rule conditions, it will appear as inactive in the list. This prevents an alert from triggering actions if it reappears in the future. + +[role="screenshot"] +image::images/rule-details-alerts-inactive.png[Rule details page with three inactive alerts] + +If you want to suppress actions on all current and future alerts, you can mute the entire rule. Rule checks continue to run and the alert list will update as alerts activate or deactivate, but no actions will be triggered. + +[role="screenshot"] +image::images/rule-details-muting.png[Use the mute toggle to suppress all actions on current and future alerts,width=50%] + +You can also disable a rule altogether. When disabled, the rule stops running checks altogether and will clear any alerts it is tracking. You may want to disable rules that are not currently needed to reduce the load on {kib} and {es}. + +[role="screenshot"] +image::images/rule-details-disabling.png[Use the disable toggle to turn off rule checks and clear alerts tracked] diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc index 05885f1af13ba2..686a7bbc8a37b9 100644 --- a/docs/user/alerting/defining-rules.asciidoc +++ b/docs/user/alerting/defining-rules.asciidoc @@ -2,114 +2,10 @@ [[defining-alerts]] == Defining rules -{kib} alerting rules can be created in a variety of apps including <>, <>, <>, <>, <> and from the <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring rules that this section describes in more detail. - -[float] -=== Create a rule - -When you create a rule, you must define the rule details, conditions, and actions. - -. <> -. <> -. <> - -image::images/rule-flyout-sections.png[The three sections of a rule definition] +This content has been moved to <>. [float] [[defining-alerts-general-details]] -=== General rule details - -All rules share the following four properties. - -[role="screenshot"] -image::images/rule-flyout-general-details.png[alt='All rules have name, tags, check every, and notify properties in common'] - -Name:: The name of the rule. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable rule listing in the management UI. A distinctive name can help identify and find a rule. -Tags:: A list of tag names that can be applied to a rule. Tags can help you organize and find rules, because tags appear in the rule listing in the management UI which is searchable by tag. -Check every:: This value determines how frequently the rule conditions below are checked. Note that the timing of background rule checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. -Notify:: This value limits how often actions are repeated when an alert remains active across rule checks. See <> for more information. + -- **Only on status change**: Actions are not repeated when an alert remains active across checks. Actions run only when the alert status changes. -- **Every time alert is active**: Actions are repeated when an alert remains active across checks. -- **On a custom action interval**: Actions are suppressed for the throttle interval, but repeat when an alert remains active across checks for a duration longer than the throttle interval. - - -[float] -[[defining-alerts-type-conditions]] -=== Rule type and conditions - -Depending upon the {kib} app and context, you may be prompted to choose the type of rule you wish to create. Some apps will pre-select the type of rule for you. - -[role="screenshot"] -image::images/rule-flyout-rule-type-selection.png[Choosing the type of rule to create] - -Each rule type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold rule the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. - -[role="screenshot"] -image::images/rule-flyout-rule-conditions.png[UI for defining rule conditions on an index threshold rule] - -[float] -[[defining-alerts-actions-details]] -=== Action type and action details - -To add an action to a rule, you first select the type of connector: - -[role="screenshot"] -image::images/rule-flyout-connector-type-selection.png[UI for selecting an action type] - -When an alert matches a condition, the rule is marked as _Active_ and assigned an action group. The actions in that group are triggered. -When the condition is no longer detected, the rule is assigned to the _Recovered_ action group, which triggers any actions assigned to that group. - -**Run When** allows you to assign an action to an action group. This will trigger the action in accordance with your **Notify** setting. - -Each action must specify a <> instance. If no connectors exist for that action type, click *Add connector* to create one. - -Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. - -[role="screenshot"] -image::images/rule-flyout-action-details.png[UI for defining an email action] - -[float] -[[defining-alerts-actions-variables]] -==== Action variables -Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass rule values at the time a condition is detected to an action. You can access the list of available variables using the "add variable" button. Although available variables differ by rule type, all rule types pass the following variables: - -`rule.id`:: The ID of the rule. -`rule.name`:: The name of the rule. -`rule.spaceId`:: The ID of the space for the rule. -`rule.tags`:: The list of tags applied to the rule. -`date`:: The date the rule scheduled the action, in ISO format. -`alert.id`:: The ID of the alert that scheduled the action. -`alert.actionGroup`:: The ID of the action group of the alert that scheduled the action. -`alert.actionSubgroup`:: The action subgroup of the alert that scheduled the action. -`alert.actionGroupName`:: The name of the action group of the alert that scheduled the action. -`kibanaBaseUrl`:: The configured <>. If not configured, this will be empty. - -[role="screenshot"] -image::images/rule-flyout-action-variables.png[Passing rule values to an action] - -Some cases exist where the variable values will be "escaped", when used in a context where escaping is needed: - -- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Markdown. -- For the <> connector, the `message` action configuration property escapes any characters that would be interpreted as Slack Markdown. -- For the <> connector, the `body` action configuration property escapes any characters that are invalid in JSON string values. - -Mustache also supports "triple braces" of the form `{{{variable name}}}`, which indicates no escaping should be done at all. Care should be used when using this form, as it could end up rendering the variable content in such a way as to make the resulting parameter invalid or formatted incorrectly. - -Each rule type defines additional variables as properties of the variable `context`. For example, if a rule type defines a variable `value`, it can be used in an action parameter as `{{context.value}}`. - -For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the rule type. - -You can attach more than one action. Clicking the "Add action" button will prompt you to select another rule type and repeat the above steps again. - -[role="screenshot"] -image::images/rule-flyout-add-action.png[You can add multiple actions on a rule] - -[NOTE] -============================================== -Actions are not required on rules. You can run a rule without actions to understand its behavior, and then <> later. -============================================== - -[float] -=== Manage rules +==== General rule details -To modify a rule after it was created, including muting or disabling it, use the <>. +This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/domain-specific-rules.asciidoc b/docs/user/alerting/domain-specific-rules.asciidoc deleted file mode 100644 index f509f9e5288234..00000000000000 --- a/docs/user/alerting/domain-specific-rules.asciidoc +++ /dev/null @@ -1,20 +0,0 @@ -[role="xpack"] -[[domain-specific-rules]] -== Domain-specific rules - -For domain-specific rules, refer to the documentation for that app. -{kib} supports these rules: - -* {observability-guide}/create-alerts.html[Observability rules] -* {security-guide}/prebuilt-rules.html[Security rules] -* <> -* {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] - -[NOTE] -============================================== -Some rule types are subscription features, while others are free features. -For a comparison of the Elastic subscription levels, -see {subscriptions}[the subscription page]. -============================================== - -include::map-rules/geo-rule-types.asciidoc[] diff --git a/docs/user/alerting/images/individual-mute-disable.png b/docs/user/alerting/images/individual-mute-disable.png index 0ed2bfc0186c08..c9d8cd666f1d84 100644 Binary files a/docs/user/alerting/images/individual-mute-disable.png and b/docs/user/alerting/images/individual-mute-disable.png differ diff --git a/docs/user/alerting/images/rule-flyout-action-no-connector.png b/docs/user/alerting/images/rule-flyout-action-no-connector.png new file mode 100644 index 00000000000000..b8b0864e04226d Binary files /dev/null and b/docs/user/alerting/images/rule-flyout-action-no-connector.png differ diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index f8a5aacce8f0e1..9ab6a2dc46ebf2 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,7 +1,7 @@ include::alerting-getting-started.asciidoc[] +include::alerting-setup.asciidoc[] +include::create-and-manage-rules.asciidoc[] include::defining-rules.asciidoc[] include::rule-management.asciidoc[] -include::rule-details.asciidoc[] -include::stack-rules.asciidoc[] -include::domain-specific-rules.asciidoc[] +include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-details.asciidoc b/docs/user/alerting/rule-details.asciidoc deleted file mode 100644 index 6e743595e5c33b..00000000000000 --- a/docs/user/alerting/rule-details.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[role="xpack"] -[[rule-details]] -== Rule details - - -The *Rule details* page tells you about the state of the rule and provides granular control over the actions it is taking. - -[role="screenshot"] -image::images/rule-details-alerts-active.png[Rule details page with three alerts] - -In this example, the rule detects when a site serves more than a threshold number of bytes in a 24 hour period. Three sites are above the threshold. These are called alerts - occurrences of the condition being detected - and the alert name, status, time of detection, and duration of the condition are shown in this view. - -Upon detection, each alert can trigger one or more actions. If the condition persists, the same actions will trigger either on the next scheduled rule check, or (if defined) after the re-notify period on the rule has passed. To prevent re-notification, you can suppress future actions by clicking on the eye icon to mute an individual alert. Muting means that the rule checks continue to run on a schedule, but that alert will not trigger any action. - -[role="screenshot"] -image::images/rule-details-alert-muting.png[Muting an alert] - -Alerts will come and go from the list depending on whether they meet the rule conditions or not - unless they are muted. If a muted instance no longer meets the rule conditions, it will appear as inactive in the list. This prevents an alert from triggering actions if it reappears in the future. - -[role="screenshot"] -image::images/rule-details-alerts-inactive.png[Rule details page with three inactive alerts] - -If you want to suppress actions on all current and future alerts, you can mute the entire rule. Rule checks continue to run and the alert list will update as alerts activate or deactivate, but no actions will be triggered. - -[role="screenshot"] -image::images/rule-details-muting.png[Use the mute toggle to suppress all actions on current and future alerts] - -You can also disable a rule altogether. When disabled, the rule stops running checks altogether and will clear any alerts it is tracking. You may want to disable rules that are not currently needed to reduce the load on {kib} and {es}. - -[role="screenshot"] -image::images/rule-details-disabling.png[Use the disable toggle to turn off rule checks and clear alerts tracked] - -* For further information on alerting concepts and examples, see <>. diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc index b908bd03b09927..d6349a60e08eb7 100644 --- a/docs/user/alerting/rule-management.asciidoc +++ b/docs/user/alerting/rule-management.asciidoc @@ -2,62 +2,4 @@ [[alert-management]] == Managing rules - -The *Rules* tab provides a cross-app view of alerting. Different {kib} apps like {observability-guide}/create-alerts.html[*Observability*], {security-guide}/prebuilt-rules.html[*Security*], <> and <> can offer their own rules. The *Rules* tab provides a central place to: - -* <> rules -* <> including enabling/disabling, muting/unmuting, and deleting -* Drill-down to <> - -[role="screenshot"] -image:images/rules-and-connectors-ui.png[Example rule listing in the Rules and Connectors UI] - -For more information on alerting concepts and the types of rules and connectors available, see <>. - -[float] -=== Finding rules - -The *Rules* tab lists all rules in the current space, including summary information about their execution frequency, tags, and type. - -The *search bar* can be used to quickly find rules by name or tag. - -[role="screenshot"] -image::images/rules-filter-by-search.png[Filtering the rules list using the search bar] - -The *type* dropdown lets you filter to a subset of rule types. - -[role="screenshot"] -image::images/rules-filter-by-type.png[Filtering the rules list by types of rule] - -The *Action type* dropdown lets you filter by the type of action used in the rule. - -[role="screenshot"] -image::images/rules-filter-by-action-type.png[Filtering the rule list by type of action] - -[float] -[[create-edit-rules]] -=== Creating and editing rules - -Many rules must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic rule types can be created in the *Rules* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting a rule type and configuring its properties. Refer to <> for details on what types of rules are available and how to configure them. - -After a rule is created, you can re-open the flyout and change a rule's properties by clicking the *Edit* button shown on each row of the rule listing. - - -[float] -[[controlling-rules]] -=== Controlling rules - -The rule listing allows you to quickly mute/unmute, disable/enable, and delete individual rules by clicking the action button. - -[role="screenshot"] -image:images/individual-mute-disable.png[The actions button allows an individual rule to be muted, disabled, or deleted] - -These operations can also be performed in bulk by multi-selecting rules and clicking the *Manage rules* button: - -[role="screenshot"] -image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk] - -[float] -=== Required permissions - -Access to rules is granted based on your privileges to alerting-enabled features. See <> for more information. +This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc new file mode 100644 index 00000000000000..bb840014fe80fb --- /dev/null +++ b/docs/user/alerting/rule-types.asciidoc @@ -0,0 +1,56 @@ +[role="xpack"] +[[rule-types]] +== Rule types + +A rule is a set of <>, <>, and <> that enable notifications. {kib} provides two types of rules: rules specific to the Elastic Stack and rules specific to a domain. + +[NOTE] +============================================== +Some rule types are subscription features, while others are free features. +For a comparison of the Elastic subscription levels, +see {subscriptions}[the subscription page]. +============================================== + +[float] +[[stack-rules]] +=== Stack rules + +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. + +[cols="2*<"] +|=== + +| <> +| Aggregate field values from documents using {es} queries, compare them to threshold values, and schedule actions to run when the thresholds are met. + +| <> +| Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met. + +|=== + +[float] +[[domain-specific-rules]] +=== Domain rules + +Domain rules are registered by *Observability*, *Security*, <> and <>. + +[cols="2*<"] +|=== + +| {observability-guide}/create-alerts.html[Observability rules] +| Detect complex conditions in the *Logs*, *Metrics*, and *Uptime* apps. + +| {security-guide}/prebuilt-rules.html[Security rules] +| Detect suspicous source events with pre-built or custom rules and create alerts when a rule’s conditions are met. + +| <> +| Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met. + +| {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] +| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. + +|=== + +include::rule-types/index-threshold.asciidoc[] +include::rule-types/es-query.asciidoc[] +include::rule-types/geo-rule-types.asciidoc[] diff --git a/docs/user/alerting/stack-rules/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc similarity index 87% rename from docs/user/alerting/stack-rules/es-query.asciidoc rename to docs/user/alerting/rule-types/es-query.asciidoc index c62ebbf4bf2bcc..5615c79a6c9c74 100644 --- a/docs/user/alerting/stack-rules/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -7,7 +7,7 @@ The {es} query rule type runs a user-configured {es} query, compares the number [float] ==== Create the rule -Fill in the <>, then select *{es} query*. +Fill in the <>, then select *{es} query*. [float] ==== Define the conditions @@ -22,12 +22,12 @@ Size:: This clause specifies the number of documents to pass to the configured a {es} query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold condition. Aggregations are not supported at this time. Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. -Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <>, to avoid gaps in detection. +Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <>, to avoid gaps in detection. [float] ==== Add action variables -<> to run when the rule condition is met. The following variables are specific to the {es} query rule. You can also specify <>. +<> to run when the rule condition is met. The following variables are specific to the {es} query rule. You can also specify <>. `context.title`:: A preconstructed title for the rule. Example: `rule term match alert query matched`. `context.message`:: A preconstructed message for the rule. Example: + diff --git a/docs/user/alerting/map-rules/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc similarity index 74% rename from docs/user/alerting/map-rules/geo-rule-types.asciidoc rename to docs/user/alerting/rule-types/geo-rule-types.asciidoc index 4b17145c2d1493..244cf90c855a7e 100644 --- a/docs/user/alerting/map-rules/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -1,16 +1,14 @@ [role="xpack"] [[geo-alerting]] -=== Geo rule type +=== Tracking containment -Alerting now includes one additional stack rule: <>. - -As with other stack rules, you need `all` access to the *Stack Rules* feature -to be able to create and edit a geo rule. -See <> for more information on configuring roles that provide access to this feature. +<> offers the Tracking containment rule type which runs an {es} query over indices to determine whether any +documents are currently contained within any boundaries from the specified boundary index. +In the event that an entity is contained within a boundary, an alert may be generated. [float] -==== Geo alerting requirements -To create a *Tracking containment* rule, the following requirements must be present: +==== Requirements +To create a Tracking containment rule, the following requirements must be present: - *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` @@ -29,22 +27,12 @@ than the current time minus the amount of the interval. If data older than `now - ` is ingested, it won't trigger a rule. [float] -==== Creating a geo rule -Click the *Create* button in the <>. -Complete the <>. - -[role="screenshot"] -image::user/alerting/images/alert-types-tracking-select.png[Choosing a tracking rule type] +==== Create the rule -[float] -[[rule-type-tracking-containment]] -==== Tracking containment -The Tracking containment rule type runs an {es} query over indices, determining if any -documents are currently contained within any boundaries from the specified boundary index. -In the event that an entity is contained within a boundary, an alert may be generated. +Fill in the <>, then select Tracking containment. [float] -===== Defining the conditions +==== Define the conditions Tracking containment rules have 3 clauses that define the condition to detect, as well as 2 Kuery bars used to provide additional filtering context for each of the indices. @@ -61,6 +49,9 @@ Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_sha identifying boundaries, and an optional *Human-readable boundary name* for better alerting messages. +[float] +==== Add action + Conditions for how a rule is tracked can be specified uniquely for each individual action. A rule can be triggered either when a containment condition is met or when an entity is no longer contained. diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc similarity index 88% rename from docs/user/alerting/stack-rules/index-threshold.asciidoc rename to docs/user/alerting/rule-types/index-threshold.asciidoc index 43b750b85fb3bd..8c45c158414f4b 100644 --- a/docs/user/alerting/stack-rules/index-threshold.asciidoc +++ b/docs/user/alerting/rule-types/index-threshold.asciidoc @@ -7,7 +7,7 @@ The index threshold rule type runs an {es} query. It aggregates field values fro [float] ==== Create the rule -Fill in the <>, then select *Index Threshold*. +Fill in the <>, then select *Index Threshold*. [float] ==== Define the conditions @@ -19,9 +19,9 @@ image::user/alerting/images/rule-types-index-threshold-conditions.png[Five claus Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. When:: This clause specifies how the value to be compared to the threshold is calculated. The value is calculated by aggregating a numeric field a the *time window*. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used, and an aggregation field is not necessary. -Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. +Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. Threshold:: This clause defines a threshold value and a comparison operator (one of `is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The result of the aggregation is compared to this threshold. -Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be to a value higher than the *check every* value in the <>, to avoid gaps in detection. +Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be to a value higher than the *check every* value in the <>, to avoid gaps in detection. If data is available and all clauses have been defined, a preview chart will render the threshold value and display a line chart showing the value for the last 30 intervals. This can provide an indication of recent values and their proximity to the threshold, and help you tune the clauses. @@ -31,7 +31,7 @@ image::user/alerting/images/rule-types-index-threshold-preview.png[Five clauses [float] ==== Add action variables -<> to run when the rule condition is met. The following variables are specific to the index threshold rule. You can also specify <>. +<> to run when the rule condition is met. The following variables are specific to the index threshold rule. You can also specify <>. `context.title`:: A preconstructed title for the rule. Example: `rule kibana sites - high egress met threshold`. `context.message`:: A preconstructed message for the rule. Example: + diff --git a/docs/user/alerting/stack-rules.asciidoc b/docs/user/alerting/stack-rules.asciidoc deleted file mode 100644 index 483834c78806e2..00000000000000 --- a/docs/user/alerting/stack-rules.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[stack-rules]] -== Stack rule types - -Kibana provides two types of rules: - -* Stack rules, which are built into {kib} -* <>, which are registered by {kib} apps. - -{kib} provides two stack rules: - -* <> -* <> - -Users require the `all` privilege to access the *Stack Rules* feature and create and edit rules. -See <> for more information. - -[NOTE] -============================================== -Some rule types are subscription features, while others are free features. -For a comparison of the Elastic subscription levels, -see {subscriptions}[the subscription page]. -============================================== - - -include::stack-rules/index-threshold.asciidoc[] -include::stack-rules/es-query.asciidoc[] diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc index 7d5547fe3c3c50..39e596df4af347 100644 --- a/docs/user/dashboard/aggregation-reference.asciidoc +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -188,6 +188,12 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | Type | Agg-based | Markdown | Lens | TSVB +| Metrics with filters +| +^| X +| +| + | Average ^| X ^| X @@ -221,7 +227,7 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | Percentiles ^| X ^| X -| +^| X ^| X | Percentiles Rank @@ -230,10 +236,10 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati | ^| X -| Top hit +| Top hit (Last value) +^| X ^| X ^| X -| ^| X | Value count @@ -266,7 +272,7 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat | Derivative ^| X ^| X -| +^| X ^| X | Max bucket @@ -290,13 +296,13 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat | Moving average ^| X ^| X -| +^| X ^| X | Cumulative sum ^| X ^| X -| +^| X ^| X | Bucket script diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 3b3a7a9ee527d4..9f17a380bc209a 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -147,14 +147,24 @@ For the answers to common *Lens* questions, review the following. [float] [[kql-]] -===== When should I use the Filter function instead of KQL filters? +===== When should I use the top filter bar, filters function, or "Filter by"? -The easiest way to apply KQL filters is to use <>, but you can also use the *Filters* function in the following scenarios: +Using the top <> bar is best when you want to focus on a known set of +data for all the visualization results. These top level filters are combined with other filters +using AND logic. + +Use the *Filters* function in the following scenarios: * When you want to apply more than one KQL filter to the visualization. * When you want to apply the KQL filter to a single layer, which allows you to visualize filtered and unfiltered data. +Use the *Filter by* advanced option in the following scenarios: + +* When you want to assign a custom color to each filter in a bar, line or area chart. + +* When you want to build a complex table, such as showing both failure rate and overall. + [float] [[when-should-i-normalize-the-data-by-unit-or-use-a-custom-interval]] ===== When should I normalize the data by unit or use a custom interval? diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index 25780d303eec41..82ca11f2162fda 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -195,7 +195,7 @@ When the rule triggers, you can send a notification to a system that is part of your daily workflow. {kib} integrates with email, Slack, PagerDuty, and ServiceNow, to name a few. -A dedicated view for creating, searching, and editing rules is in <>. +A dedicated view for creating, searching, and editing rules is in <>. [role="screenshot"] image::images/rules-and-connectors.png[Rules and Connectors view] @@ -437,7 +437,7 @@ the <>. |< Data>> |Set up rules -|< Rules and Connectors>> +|< Rules and Connectors>> |Organize your workspace and users |< Spaces>> diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index c5fabb15dc4de2..b86fa82c30381b 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -75,7 +75,7 @@ You can add and remove remote clusters, and check their connectivity. |=== | <> -| Centrally <> across {kib}. Create and <> across {kib}. Create and <> for triggering actions. | <> diff --git a/docs/user/monitoring/images/monitoring-kibana-alerting-notification.png b/docs/user/monitoring/images/monitoring-kibana-alerting-notification.png new file mode 100644 index 00000000000000..90951d18e667b6 Binary files /dev/null and b/docs/user/monitoring/images/monitoring-kibana-alerting-notification.png differ diff --git a/docs/user/monitoring/images/monitoring-kibana-alerting-setup-mode.png b/docs/user/monitoring/images/monitoring-kibana-alerting-setup-mode.png new file mode 100644 index 00000000000000..146992da5837ab Binary files /dev/null and b/docs/user/monitoring/images/monitoring-kibana-alerting-setup-mode.png differ diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 58bf419d8d54a9..6046e67db62f1d 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -1,100 +1,109 @@ [role="xpack"] [[kibana-alerts]] -= {kib} Alerts += {kib} alerts The {stack} {monitor-features} provide -<> out-of-the box to notify you of -potential issues in the {stack}. These alerts are preconfigured based on the +<> out-of-the box to notify you +of potential issues in the {stack}. These rules are preconfigured based on the best practices recommended by Elastic. However, you can tailor them to meet your specific needs. -When you open *{stack-monitor-app}*, the preconfigured {kib} alerts are -created automatically. If you collect monitoring data from multiple clusters, -these alerts can search, detect, and notify on various conditions across the -clusters. The alerts are visible alongside your existing {watcher} cluster -alerts. You can view details about the alerts that are active and view health -and performance data for {es}, {ls}, and Beats in real time, as well as -analyze past performance. You can also modify active alerts. +[role="screenshot"] +image::user/monitoring/images/monitoring-kibana-alerts.png["{kib} alerts in {stack-monitor-app}"] + +When you open *{stack-monitor-app}*, the preconfigured rules are created +automatically. They are initially configured to detect and notify on various +conditions across your monitored clusters. You can view notifications for: *Cluster health*, *Resource utilization*, and *Errors and exceptions* for {es} +in real time. + +NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have +been recreated as rules in {kib} {alert-features}. For this reason, the existing +{watcher} email action +`monitoring.cluster_alerts.email_notifications.email_address` no longer works. +The default action for all {stack-monitor-app} rules is to write to {kib} logs +and display a notification in the UI. [role="screenshot"] -image::user/monitoring/images/monitoring-kibana-alerts.png["Kibana alerts in the Stack Monitoring app"] +image::user/monitoring/images/monitoring-kibana-alerting-notification.png["{kib} alerting notifications in {stack-monitor-app}"] -To review and modify all the available alerts, use -<> in *{stack-manage-app}*. + +[role="screenshot"] +image::user/monitoring/images/monitoring-kibana-alerting-setup-mode.png["Modify {kib} alerting rules in {stack-monitor-app}"] [discrete] [[kibana-alerts-cpu-threshold]] -== CPU threshold +== CPU usage threshold -This alert is triggered when a node runs a consistently high CPU load. By -default, the trigger condition is set at 85% or more averaged over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. +This rule checks for {es} nodes that run a consistently high CPU load. By +default, the condition is set at 85% or more averaged over the last 5 minutes. +The rule is grouped across all the nodes of the cluster by running checks on a +schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-disk-usage-threshold]] == Disk usage threshold -This alert is triggered when a node is nearly at disk capacity. By -default, the trigger condition is set at 80% or more averaged over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. +This rule checks for {es} nodes that are nearly at disk capacity. By default, +the condition is set at 80% or more averaged over the last 5 minutes. The rule +is grouped across all the nodes of the cluster by running checks on a schedule +time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-jvm-memory-threshold]] == JVM memory threshold -This alert is triggered when a node runs a consistently high JVM memory usage. By -default, the trigger condition is set at 85% or more averaged over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. +This rule checks for {es} nodes that use a high amount of JVM memory. By +default, the condition is set at 85% or more averaged over the last 5 minutes. +The rule is grouped across all the nodes of the cluster by running checks on a +schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-missing-monitoring-data]] == Missing monitoring data -This alert is triggered when any stack products nodes or instances stop sending -monitoring data. By default, the trigger condition is set to missing for 15 minutes -looking back 1 day. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +This rule checks for {es} nodes that stop sending monitoring data. By default, +the condition is set to missing for 15 minutes looking back 1 day. The rule is +grouped across all the {es} nodes of the cluster by running checks on a schedule +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-thread-pool-rejections]] == Thread pool rejections (search/write) -This alert is triggered when a node experiences thread pool rejections. By -default, the trigger condition is set at 300 or more over the last 5 -minutes. The alert is grouped across all the nodes of the cluster by running -checks on a schedule time of 1 minute with a re-notify interval of 1 day. -Thresholds can be set independently for `search` and `write` type rejections. +This rule checks for {es} nodes that experience thread pool rejections. By +default, the condition is set at 300 or more over the last 5 minutes. The rule +is grouped across all the nodes of the cluster by running checks on a schedule +time of 1 minute with a re-notify interval of 1 day. Thresholds can be set +independently for `search` and `write` type rejections. [discrete] [[kibana-alerts-ccr-read-exceptions]] == CCR read exceptions -This alert is triggered if a read exception has been detected on any of the -replicated clusters. The trigger condition is met if 1 or more read exceptions -are detected in the last hour. The alert is grouped across all replicated clusters -by running checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +This rule checks for read exceptions on any of the replicated {es} clusters. The +condition is met if 1 or more read exceptions are detected in the last hour. The +rule is grouped across all replicated clusters by running checks on a schedule +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-large-shard-size]] == Large shard size -This alert is triggered if a large average shard size (across associated primaries) is found on any of the -specified index patterns. The trigger condition is met if an index's average shard size is -55gb or higher in the last 5 minutes. The alert is grouped across all indices that match -the default pattern of `*` by running checks on a schedule time of 1 minute with a re-notify -interval of 12 hours. +This rule checks for a large average shard size (across associated primaries) on +any of the specified index patterns in an {es} cluster. The condition is met if +an index's average shard size is 55gb or higher in the last 5 minutes. The rule +is grouped across all indices that match the default pattern of `-.*` by running +checks on a schedule time of 1 minute with a re-notify interval of 12 hours. [discrete] [[kibana-alerts-cluster-alerts]] -== Cluster alerts +== Cluster alerting -These alerts summarize the current status of your {stack}. You can drill down into the metrics -to view more information about your cluster and specific nodes, instances, and indices. +These rules check the current status of your {stack}. You can drill down into +the metrics to view more information about your cluster and specific nodes, instances, and indices. -An alert will be triggered if any of the following conditions are met within the last minute: +An action is triggered if any of the following conditions are met within the +last minute: * {es} cluster health status is yellow (missing at least one replica) or red (missing at least one primary). @@ -110,7 +119,7 @@ versions reporting stats to the same monitoring cluster. -- If you do not preserve the data directory when upgrading a {kib} or Logstash node, the instance is assigned a new persistent UUID and shows up -as a new instance +as a new instance. -- * Subscription license expiration. When the expiration date approaches, you will get notifications with a severity level relative to how diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index 6294a4fe6f14ae..bd19a11435a99d 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -19,7 +19,7 @@ When relying on rules and actions as mission critical services, make sure you fo By default, each {kib} instance polls for work at three second intervals, and can run a maximum of ten concurrent tasks. These tasks are then run on the {kib} server. -Rules are recurring background tasks which are rescheduled according to the <> on completion. +Rules are recurring background tasks which are rescheduled according to the <> on completion. Actions are non-recurring background tasks which are deleted on completion. For more details on Task Manager, see <>. @@ -42,7 +42,7 @@ As rules and actions leverage background tasks to perform the majority of work, When estimating the required task throughput, keep the following in mind: -* Each rule uses a single recurring task that is scheduled to run at the cadence defined by its <>. +* Each rule uses a single recurring task that is scheduled to run at the cadence defined by its <>. * Each action uses a single task. However, because <>, alerts can generate a large number of non-recurring tasks. It is difficult to predict how much throughput is needed to ensure all rules and actions are executed at consistent schedules. diff --git a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc index d6b90a4f19e112..8f2c8d106c77cf 100644 --- a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc +++ b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc @@ -92,10 +92,18 @@ a| Runtime | This section tracks excution performance of Task Manager, tracking task _drift_, worker _load_, and execution stats broken down by type, including duration and execution results. +a| Capacity Estimation + +| This section provides a rough estimate about the sufficiency of its capacity. As the name suggests, these are estimates based on historical data and should not be used as predictions. Use these estimations when following the Task Manager <>. + |=== Each section has a `timestamp` and a `status` that indicates when the last update to this section took place and whether the health of this section was evaluated as `OK`, `Warning` or `Error`. The root `status` indicates the `status` of the system overall. +The Runtime `status` indicates whether task executions have exceeded any of the <>. An `OK` status means none of the threshold have been exceeded. A `Warning` status means that at least one warning threshold has been exceeded. An `Error` status means that at least one error threshold has been exceeded. + +The Capacity Estimation `status` indicates the sufficiency of the observed capacity. An `OK` status means capacity is sufficient. A `Warning` status means that capacity is sufficient for the scheduled recurring tasks, but non-recurring tasks often cause the cluster to exceed capacity. An `Error` status means that there is insufficient capacity across all types of tasks. + By monitoring the `status` of the system overall, and the `status` of specific task types of interest, you can evaluate the health of the {kib} Task Management system. diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 606f113b2274f1..17eae59ff2f9c2 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -68,11 +68,7 @@ This means that you can expect a single {kib} instance to support up to 200 _tas In practice, a {kib} instance will only achieve the upper bound of `200/tpm` if the duration of task execution is below the polling rate of 3 seconds. For the most part, the duration of tasks is below that threshold, but it can vary greatly as {es} and {kib} usage grow and task complexity increases (such as alerts executing heavy queries across large datasets). -By <>, you can make a rough estimate as to the required throughput as a _tasks per minute_ measurement. - -For example, suppose your current workload reveals a required throughput of `440/tpm`. You can address this scale by provisioning 3 {kib} instances, with an upper throughput of `600/tpm`. This scale would provide aproximately 25% additional capacity to handle ad-hoc non-recurring tasks and potential growth in recurring tasks. - -It is highly recommended that you maintain at least 20% additional capacity, beyond your expected workload, as spikes in ad-hoc tasks is possible at times of high activity (such as a spike in actions in response to an active alert). +By <>, you can estimate the number of {kib} instances required to reliably execute tasks in a timely manner. An appropriate number of {kib} instances can be estimated to match the required scale. For details on monitoring the health of {kib} Task Manager, follow the guidance in <>. @@ -126,6 +122,35 @@ Throughput is best thought of as a measurements in tasks per minute. A default {kib} instance can support up to `200/tpm`. +[float] +===== Automatic estimation + +experimental[] + +As demonstrated in <>, the Task Manager <> performs these estimations automatically. + +These estimates are based on historical data and should not be used as predictions, but can be used as a rough guide when scaling the system. + +We recommend provisioning enough {kib} instances to ensure a buffer between the observed maximum throughput (as estimated under `observed.max_throughput_per_minute`) and the average required throughput (as estimated under `observed.avg_required_throughput_per_minute`). Otherwise there might be insufficient capacity to handle spikes of ad-hoc tasks. How much of a buffer is needed largely depends on your use case, but keep in mind that estimated throughput takes into account recent spikes and, as long as they are representative of your system's behaviour, shouldn't require much of a buffer. + +We recommend provisioning at least as many {kib} instances as proposed by `proposed.provisioned_kibana`, but keep in mind that this number is based on the estimated required throughput, which is based on average historical performance, and cannot accurately predict future requirements. + +[WARNING] +============================================================================ +Automatic capacity estimation is performed by each {kib} instance independently. This estimation is performed by observing the task throughput in that instance, the number of {kib} instances executing tasks at that moment in time, and the recurring workload in {es}. + +If a {kib} instance is idle at the moment of capacity estimation, the number of active {kib} instances might be miscounted and the available throughput miscalculated. + +When evaluating the proposed {kib} instance number under `proposed.provisioned_kibana`, we highly recommend verifying that the `observed.observed_kibana_instances` matches the number of provisioned {kib} instances. +============================================================================ + +[float] +===== Manual estimation + +By <>, you can make a rough estimate as to the required throughput as a _tasks per minute_ measurement. + +For example, suppose your current workload reveals a required throughput of `440/tpm`. You can address this scale by provisioning 3 {kib} instances, with an upper throughput of `600/tpm`. This scale would provide aproximately 25% additional capacity to handle ad-hoc non-recurring tasks and potential growth in recurring tasks. + Given a deployment of 100 recurring tasks, estimating the required throughput depends on the scheduled cadence. Suppose you expect to run 50 tasks at a cadence of `10s`, the other 50 tasks at `20m`. In addition, you expect a couple dozen non-recurring tasks every minute. @@ -136,8 +161,11 @@ A recurring task requires as many executions as its cadence can fit in a minute. For this reason, we recommend grouping tasks by _tasks per minute_ and _tasks per hour_, as demonstrated in <>, averaging the _per hour_ measurement across all minutes. +It is highly recommended that you maintain at least 20% additional capacity, beyond your expected workload, as spikes in ad-hoc tasks is possible at times of high activity (such as a spike in actions in response to an active alert). + Given the predicted workload, you can estimate a lower bound throughput of `340/tpm` (`6/tpm` * 50 + `3/tph` * 50 + 20% buffer). As a default, a {kib} instance provides a throughput of `200/tpm`. A good starting point for your deployment is to provision 2 {kib} instances. You could then monitor their performance and reassess as the required throughput becomes clearer. Although this is a _rough_ estimate, the _tasks per minute_ provides the lower bound needed to execute tasks on time. -Once you calculate the rough _tasks per minute_ estimate, add a 20% buffer for non-recurring tasks. How much of a buffer is required largely depends on your use case, so <> as it grows to ensure enough of a buffer is provisioned. + +Once you estimate _tasks per minute_ , add a buffer for non-recurring tasks. How much of a buffer is required largely depends on your use case. Ensure enough of a buffer is provisioned by <> as it grows and tracking the ratio of recurring to non-recurring tasks by <>. diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc index c6a7b7f3d53fdc..4b63313b2b96e0 100644 --- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc +++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc @@ -74,6 +74,7 @@ By analyzing the different sections of the output, you can evaluate different th ** <> ** <> * <> +* <> Retrieve the latest monitored health stats of a {kib} instance Task Manager: @@ -178,6 +179,11 @@ The API returns the following: "p99": 166 } }, + "persistence": { + "recurring": 88, + "non_recurring": 4, + "ephemeral": 8 + }, "result_frequency_percent_as_number": { "alerting:.index-threshold": { "Success": 100, @@ -233,12 +239,44 @@ The API returns the following: ["1m", 2], ["60s", 2], ["5m", 2], - ["60m", 4] + ["60m", 4], + ["3600s", 1], + ["720m", 1] ], - "overdue": 0, - "estimated_schedule_density": [0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0] + "non_recurring": 18, + "owner_ids": 0, + "overdue": 10, + "overdue_non_recurring": 10, + "estimated_schedule_density": [0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0], + "capacity_requirments": { + "per_minute": 6, + "per_hour": 28, + "per_day": 2 + } }, "status": "OK" + }, + "capacity_estimation": { + "timestamp": "2021-02-16T11:38:06.826Z", + "value": { + "observed": { + "observed_kibana_instances": 1, + "max_throughput_per_minute_per_kibana": 200, + "max_throughput_per_minute": 200, + "minutes_to_drain_overdue": 1, + "avg_recurring_required_throughput_per_minute": 28, + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute": 28, + "avg_required_throughput_per_minute_per_kibana": 28 + }, + "proposed": { + "min_required_kibana": 1, + "provisioned_kibana": 1, + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute_per_kibana": 28 + } + } + "status": "OK" } } } @@ -530,7 +568,7 @@ Evaluating the health stats in this hypothetical scenario, you see the following You can infer from these stats that the high drift the Task Manager is experiencing is most likely due to Elasticsearch query alerts that are running for a long time. Resolving this issue is context dependent and changes from case to case. -In the preceding example above, this would be resolved by modifying the queries in these alerts to make them faster, or improving the {es} throughput to speed up the exiting query. +In the preceding example, this would be resolved by modifying the queries in these alerts to make them faster, or improving the {es} throughput to speed up the exiting query. [[task-manager-theory-high-fail-rate]] *Theory*: @@ -571,6 +609,82 @@ Evaluating the preceding health stats, you see the following output under `stats You can infer from these stats that most `actions:.index` tasks, which back the ES Index {kib} action, fail. Resolving that would require deeper investigation into the {kib} Server Log, where the exact errors are logged, and addressing these specific errors. +[[task-manager-theory-spikes-in-non-recurring-tasks]] +*Theory*: +Spikes in non-recurring and ephemeral tasks are consuming a high percentage of the available capacity + +*Diagnosis*: +Task Manager uses ad-hoc non-recurring tasks to load balance operations across multiple {kib} instances. +Additionally, {kib} can use Task Manager to allocate resources for expensive operations by executing an ephemeral task. Ephemeral tasks are identical in operation to non-recurring tasks, but are not persisted and cannot be load balanced across {kib} instances. + +Evaluating the preceding health stats, you see the following output under `stats.runtime.value.execution.persistence`: + +[source,json] +-------------------------------------------------- +{ + "recurring": 88, # <1> + "non_recurring": 4, # <2> + "ephemeral": 8 # <3> +}, +-------------------------------------------------- +<1> 88% of executed tasks are recurring tasks +<2> 4% of executed tasks are non-recurring tasks +<3> 8% of executed tasks are ephemeral tasks + +You can infer from these stats that the majority of executions consist of recurring tasks at 88%. +You can use the `execution.persistence` stats to evaluate the ratio of consumed capacity, but on their own, you should not make assumptions about the sufficiency of the available capacity. + +To assess the capacity, you should evaluate these stats against the `load` under `stats.runtime.value`: + +[source,json] +-------------------------------------------------- +{ + "load": { # <2> + "p50": 40, + "p90": 40, + "p95": 60, + "p99": 80 + } +} +-------------------------------------------------- + +You can infer from these stats that it is very unusual for Task Manager to run out of capacity, so the capacity is likely sufficient to handle the amount of non-recurring and ephemeral tasks. + +Suppose you have an alternate scenario, where you see the following output under `stats.runtime.value.execution.persistence`: + +[source,json] +-------------------------------------------------- +{ + "recurring": 60, # <1> + "non_recurring": 30, # <2> + "ephemeral": 10 # <3> +}, +-------------------------------------------------- +<1> 60% of executed tasks are recurring tasks +<2> 30% of executed tasks are non-recurring tasks +<3> 10% of executed tasks are ephemeral tasks + +You can infer from these stats that even though most executions are recurring tasks, a substantial percentage of executions are non-recurring and ephemeral tasks at 40%. + +Evaluating the `load` under `stats.runtime.value`, you see the following: + +[source,json] +-------------------------------------------------- +{ + "load": { # <2> + "p50": 70, + "p90": 100, + "p95": 100, + "p99": 100 + } +} +-------------------------------------------------- + +You can infer from these stats that it is quite common for this {kib} instance to run out of capacity. +Given the high rate of non-recurring and ephemeral tasks, it would be reasonable to assess that there is insufficient capacity in the {kib} cluster to handle the amount of tasks. + +Keep in mind that these stats give you a glimpse at a moment in time, and even though there has been insufficient capacity in recent minutes, this might not be true in other times where fewer non-recurring or ephemeral tasks are used. We recommend tracking these stats over time and identifying the source of these tasks before making sweeping changes to your infrastructure. + [[task-manager-health-evaluate-the-workload]] ===== Evaluate the Workload @@ -579,7 +693,7 @@ Predicting the required throughput a deplyment might need to support Task Manage <> provides statistics that make it easier to monitor the adequacy of the existing throughput. By evaluating the workload, the required throughput can be estimated, which is used when following the Task Manager <>. -Evaluating the preceding health stats above, you see the following output under `stats.workload.value`: +Evaluating the preceding health stats in the previous example, you see the following output under `stats.workload.value`: [source,json] -------------------------------------------------- @@ -607,27 +721,39 @@ Evaluating the preceding health stats above, you see the following output under } }, }, - "schedule": [ # <4> + "non_recurring": 0, # <4> + "owner_ids": 1, # <5> + "schedule": [ # <6> ["10s", 2], ["1m", 2], ["90s", 2], ["5m", 8] ], - "overdue": 0, # <5> - "estimated_schedule_density": [ # <6> + "overdue_non_recurring": 0, # <7> + "overdue": 0, # <8> + "estimated_schedule_density": [ # <9> 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0 - ] + ], + "capacity_requirments": { # <10> + "per_minute": 14, + "per_hour": 240, + "per_day": 0 + } } -------------------------------------------------- <1> There are 26 tasks in the system, including regular tasks, recurring tasks, and failed tasks. <2> There are 2 `idle` index threshold alert tasks, meaning they are scheduled to run at some point in the future. <3> Of the 14 tasks backing the ES index action, 10 have failed and 2 are running. -<4> A histogram of all scheduled recurring tasks shows that 2 tasks are scheduled to run every 10 seconds, 2 tasks are scheduled to run once a minute, and so on. -<5> There are no tasks overdue, which means that all tasks that *should* have run by now *have* run. -<6> This histogram shows the tasks scheduled to run throughout the upcoming 20 polling cycles. The histogram represents the entire deployment, rather than just this {kib} instance +<4> There are no non-recurring tasks in the queue. +<5> There is one Task Manager actively executing tasks. There might be additional idle Task Managers, but they aren't actively executing tasks at this moment in time. +<6> A histogram of all scheduled recurring tasks shows that 2 tasks are scheduled to run every 10 seconds, 2 tasks are scheduled to run once a minute, and so on. +<7> There are no overdue non-recurring tasks. Non-recurring tasks are usually scheduled to execute immediately, so overdue non-recurring tasks are often a symptom of a congested system. +<8> There are no overdue tasks, which means that all tasks that *should* have run by now *have* run. +<9> This histogram shows the tasks scheduled to run throughout the upcoming 20 polling cycles. The histogram represents the entire deployment, rather than just this {kib} instance. +<10> The capacity required to handle the recurring tasks in the system. These are buckets, rather than aggregated sums, and we recommend <> section, rather than evaluating these buckets yourself. The `workload` section summarizes the work load across the cluster, listing the tasks in the system, their types, schedules, and current status. @@ -674,6 +800,8 @@ Suppose the output of `stats.workload.value` looked something like this: } }, }, + "non_recurring": 0, + "owner_ids": 1, "schedule": [ # <2> ["10s", 38], ["1m", 101], @@ -683,32 +811,133 @@ Suppose the output of `stats.workload.value` looked something like this: ["60m", 106], ["1d", 61] ], + "overdue_non_recurring": 0, "overdue": 0, # <5> "estimated_schedule_density": [ # <3> 10, 1, 0, 10, 0, 20, 0, 1, 0, 1, 9, 0, 3, 10, 0, 0, 10, 10, 7, 0, 0, 31, 0, 12, 16, 31, 0, 10, 0, 10, 3, 22, 0, 10, 0, 2, 10, 10, 1, 0 - ] + ], + "capacity_requirments": { + "per_minute": 329, # <4> + "per_hour": 4272, # <5> + "per_day": 61 # <6> + } } -------------------------------------------------- <1> There are 2,191 tasks in the system. <2> The scheduled tasks are distributed across a variety of cadences. <3> The schedule density shows that you expect to exceed the default 10 concurrent tasks. +<4> There are 329 task executions that recur within the space of every minute. +<5> There are 4,273 task executions that recur within the space of every hour. +<6> There are 61 task executions that recur within the space of every day. You can infer several important attributes of your workload from this output: * There are many tasks in your system and ensuring these tasks run on their scheduled cadence will require attention to the Task Manager throughput. -* Assessing the high frequency tasks (tasks that recur at a cadence of a couple of minutes or less), you must support a throughput of approximately 400 tasks per minute (38 every 10 seconds + 101 every minute + 55 every 90 seconds). -* Assessing the medium frequency tasks (tasks that recur at a cadence of an hour or less), you must support an additional throughput of over 2000 tasks per hour (89 every 5 minutes, + 62 every 20 minutes + 106 each hour). You can average the needed throughput for the hour by counting these tasks as an additional 30 to 40 tasks per minute. +* Assessing the high frequency tasks (tasks that recur at a cadence of a couple of minutes or less), you must support a throughput of approximately 330 task executions per minute (38 every 10 seconds + 101 every minute). +* Assessing the medium frequency tasks (tasks that recur at a cadence of an hour or less), you must support an additional throughput of over 4,272 task executions per hour (55 every 90 seconds + 89 every 5 minutes, + 62 every 20 minutes + 106 each hour). You can average the needed throughput for the hour by counting these tasks as an additional 70 - 80 tasks per minute. * Assessing the estimated schedule density, there are cycles that are due to run upwards of 31 tasks concurrently, and along side these cycles, there are empty cycles. You can expect Task Manager to load balance these tasks throughout the empty cycles, but this won't leave much capacity to handle spikes in fresh tasks that might be scheduled in the future. -These rough calculations give you a lower bound to the required throughput, which is _at least_ 440 tasks per minute to ensure recurring tasks are executed, at their scheduled time. This throughput doesn't account for nonrecurring tasks that might have been scheduled, nor does it account for tasks (recurring or otherwise) that might be scheduled in the future. +These rough calculations give you a lower bound to the required throughput, which is _at least_ 410 tasks per minute to ensure recurring tasks are executed, at their scheduled time. This throughput doesn't account for nonrecurring tasks that might have been scheduled, nor does it account for tasks (recurring or otherwise) that might be scheduled in the future. Given these inferred attributes, it would be safe to assume that a single {kib} instance with default settings **would not** provide the required throughput. It is possible that scaling horizontally by adding a couple more {kib} instances will. For details on scaling Task Manager, see <>. + +[[task-manager-health-evaluate-the-capacity-estimation]] +===== Evaluate the Capacity Estimation + +Task Manager is constantly evaluating its runtime operations and workload. This enables Task Manager to make rough estimates about the sufficiency of its capacity. + +As the name suggests, these are estimates based on historical data and should not be used as predictions. These estimations should be evaluated alongside the detailed <> stats before making changes to infrastructure. These estimations assume all {kib} instances are configured identically. + +We recommend using these estimations when following the Task Manager <>. + +Evaluating the health stats in the previous example, you can see the following output under `stats.capacity_estimation.value`: + +[source,json] +-------------------------------------------------- +{ + "observed": { + "observed_kibana_instances": 1, # <1> + "minutes_to_drain_overdue": 1, # <2> + "max_throughput_per_minute_per_kibana": 200, + "max_throughput_per_minute": 200, # <3> + "avg_recurring_required_throughput_per_minute": 28, # <4> + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute": 28, # <5> + "avg_required_throughput_per_minute_per_kibana": 28 + }, + "proposed": { + "min_required_kibana": 1, # <6> + "provisioned_kibana": 1, # <7> + "avg_recurring_required_throughput_per_minute_per_kibana": 28, + "avg_required_throughput_per_minute_per_kibana": 28 + } +} +-------------------------------------------------- +<1> These estimates assume that there is one {kib} instance actively executing tasks. +<2> Based on past throughput the overdue tasks in the system could be executed within 1 minute. +<3> Assuming all {kib} instances in the cluster are configured the same as this instance, the maximum available throughput is 200 tasks per minute. +<4> On average, the recurring tasks in the system have historically required a throughput of 28 tasks per minute. +<5> On average, regardless of whether they are recurring or otherwise, the tasks in the system have historically required a throughput of 28 tasks per minute. +<6> One {kib} instance should be sufficient to run the current recurring workload. +<7> We propose waiting for the workload to change before additional {kib} instances are provisioned. + +The `capacity_estimation` section is made up of two subsections: + +* `observed` estimates the current capacity by observing historical runtime and workload statistics +* `proposed` estimates the baseline {kib} cluster size and the expected throughput under such a deployment strategy + +You can infer from these estimates that the current system is under-utilized and has enough capacity to handle many more tasks than it currently does. + +Suppose an alternate scenario, where you see the following output under `stats.capacity_estimation.value`: + +[source,json] +-------------------------------------------------- +{ + "observed": { + "observed_kibana_instances": 2, # <1> + "max_throughput_per_minute_per_kibana": 200, + "max_throughput_per_minute": 400, # <2> + "minutes_to_drain_overdue": 12, # <3> + "avg_recurring_required_throughput_per_minute": 354, # <4> + "avg_recurring_required_throughput_per_minute_per_kibana": 177, # <5> + "avg_required_throughput_per_minute": 434, # <6> + "avg_required_throughput_per_minute_per_kibana": 217 + }, + "proposed": { + "min_required_kibana": 2, # <7> + "provisioned_kibana": 3, # <8> + "avg_recurring_required_throughput_per_minute_per_kibana": 118, # <9> + "avg_required_throughput_per_minute_per_kibana": 145 # <10> + } +} +-------------------------------------------------- +<1> These estimates assume that there are two {kib} instance actively executing tasks. +<2> The maximum available throughput in the system currently is 400 tasks per minute. +<3> Based on past throughput the overdue tasks in the system should be executed within 12 minutes. +<4> On average, the recurring tasks in the system have historically required a throughput of 354 tasks per minute. +<5> On average, each {kib} instance utilizes 177 tasks per minute of its capacity to execute recurring tasks. +<6> On average the tasks in the system have historically required a throughput of 434 tasks per minute. +<7> The system estimates that at least two {kib} instances are required to run the current recurring workload. +<8> The system recommends provisioning three {kib} instances to handle the workload. +<9> Once a third {kib} instance is provisioned, the capacity utilized by each instance to execute recurring tasks should drop from 177 to 118 tasks per minute. +<10> Taking into account historical ad-hoc task execution, we estimate the throughput required of each {kib} instance will drop from 217 task per minute to 145, once a third {kib} instance is provisioned. + +Evaluating by these estimates, we can infer some interesting attributes of our system: + +* These estimates are produced based on the assumption that there are two {kib} instances in the cluster. This number is based on the number of {kib} instances actively executing tasks in recent minutes. At times this number might fluctuate if {kib} instances remain idle, so validating these estimates against what you know about the system is recommended. +* There appear to be so many overdue tasks that it would take 12 minutes of executions to catch up with that backlog. This does not take into account tasks that might become overdue during those 12 minutes. Although this congestion might be temporary, the system could also remain consistently under provisioned and might never drain the backlog entirely. +* Evaluating the recurring tasks in the workload, the system requires a throughput of 354 tasks per minute on average to execute tasks on time, which is lower then the estimated maximum throughput of 400 tasks per minute. Once we take into account historical throughput though, we estimate the required throughput at 434 tasks per minute. This suggests that, historically, approximately 20% of tasks have been ad-hoc non-recurring tasks, the scale of which are harder to predict than recurring tasks. + +You can infer from these estimates that the capacity in the current system is insufficient and at least one additional {kib} instance is required to keep up with the workload. + +For details on scaling Task Manager, see <>. + [float] [[task-manager-cannot-operate-when-inline-scripts-are-disabled]] ==== Inline scripts are disabled in {es} diff --git a/examples/screenshot_mode_example/kibana.json b/examples/screenshot_mode_example/kibana.json index 4cb8c1a1393fbd..28e5b39e5337f9 100644 --- a/examples/screenshot_mode_example/kibana.json +++ b/examples/screenshot_mode_example/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["navigation", "screenshotMode", "usageCollection"], + "requiredPlugins": ["navigation", "screenshotMode", "usageCollection", "developerExamples"], "optionalPlugins": [] } diff --git a/examples/screenshot_mode_example/public/plugin.ts b/examples/screenshot_mode_example/public/plugin.ts index 91bcc2410b5fc5..4108924ca3b8d8 100644 --- a/examples/screenshot_mode_example/public/plugin.ts +++ b/examples/screenshot_mode_example/public/plugin.ts @@ -6,7 +6,13 @@ * Side Public License, v 1. */ -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + AppNavLinkStatus, +} from '../../../src/core/public'; import { AppPluginSetupDependencies, AppPluginStartDependencies } from './types'; import { MetricsTracking } from './services'; import { PLUGIN_NAME } from '../common'; @@ -15,7 +21,7 @@ export class ScreenshotModeExamplePlugin implements Plugin { uiTracking = new MetricsTracking(); public setup(core: CoreSetup, depsSetup: AppPluginSetupDependencies): void { - const { screenshotMode, usageCollection } = depsSetup; + const { screenshotMode, usageCollection, developerExamples } = depsSetup; const isScreenshotMode = screenshotMode.isScreenshotMode(); this.uiTracking.setup({ @@ -27,6 +33,7 @@ export class ScreenshotModeExamplePlugin implements Plugin { core.application.register({ id: 'screenshotModeExample', title: PLUGIN_NAME, + navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { // Load application bundle const { renderApp } = await import('./application'); @@ -40,6 +47,13 @@ export class ScreenshotModeExamplePlugin implements Plugin { return renderApp(coreStart, depsSetup, depsStart as AppPluginStartDependencies, params); }, }); + + developerExamples.register({ + appId: 'screenshotModeExample', + title: 'Screenshot mode integration', + description: + 'Demonstrate how a plugin can adapt appearance based on whether we are in screenshot mode', + }); } public start(core: CoreStart): void {} diff --git a/examples/screenshot_mode_example/public/types.ts b/examples/screenshot_mode_example/public/types.ts index 88812a4a507c91..2eb9bd8e144a0f 100644 --- a/examples/screenshot_mode_example/public/types.ts +++ b/examples/screenshot_mode_example/public/types.ts @@ -9,10 +9,12 @@ import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { ScreenshotModePluginSetup } from '../../../src/plugins/screenshot_mode/public'; import { UsageCollectionSetup } from '../../../src/plugins/usage_collection/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; export interface AppPluginSetupDependencies { usageCollection: UsageCollectionSetup; screenshotMode: ScreenshotModePluginSetup; + developerExamples: DeveloperExamplesSetup; } export interface AppPluginStartDependencies { diff --git a/package.json b/package.json index cf6bd407d53a43..ff2f62f5130841 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "30.0.0", + "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.13.0", @@ -215,7 +215,6 @@ "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", "d3-array": "1.2.4", - "d3-cloud": "1.2.5", "d3-scale": "1.0.7", "d3-shape": "^1.1.0", "d3-time": "^1.1.0", @@ -318,7 +317,7 @@ "pegjs": "0.10.0", "pluralize": "3.1.0", "pngjs": "^3.4.0", - "polished": "^1.9.2", + "polished": "^3.7.2", "prop-types": "^15.7.2", "proper-lockfile": "^3.2.0", "proxy-from-env": "1.0.0", diff --git a/packages/kbn-apm-utils/src/index.ts b/packages/kbn-apm-utils/src/index.ts index 384b6683199e5b..09a6989091f609 100644 --- a/packages/kbn-apm-utils/src/index.ts +++ b/packages/kbn-apm-utils/src/index.ts @@ -14,6 +14,7 @@ export interface SpanOptions { type?: string; subtype?: string; labels?: Record; + intercept?: boolean; } type Span = Exclude; @@ -36,23 +37,27 @@ export async function withSpan( ): Promise { const options = parseSpanOptions(optionsOrName); - const { name, type, subtype, labels } = options; + const { name, type, subtype, labels, intercept } = options; if (!agent.isStarted()) { return cb(); } + let createdSpan: Span | undefined; + // When a span starts, it's marked as the active span in its context. // When it ends, it's not untracked, which means that if a span // starts directly after this one ends, the newly started span is a // child of this span, even though it should be a sibling. // To mitigate this, we queue a microtask by awaiting a promise. - await Promise.resolve(); + if (!intercept) { + await Promise.resolve(); - const span = agent.startSpan(name); + createdSpan = agent.startSpan(name) ?? undefined; - if (!span) { - return cb(); + if (!createdSpan) { + return cb(); + } } // If a span is created in the same context as the span that we just @@ -61,33 +66,51 @@ export async function withSpan( // mitigate this we create a new context. return runInNewContext(() => { + const promise = cb(createdSpan); + + let span: Span | undefined = createdSpan; + + if (intercept) { + span = agent.currentSpan ?? undefined; + } + + if (!span) { + return promise; + } + + const targetedSpan = span; + + if (name) { + targetedSpan.name = name; + } + // @ts-ignore if (type) { - span.type = type; + targetedSpan.type = type; } if (subtype) { - span.subtype = subtype; + targetedSpan.subtype = subtype; } if (labels) { - span.addLabels(labels); + targetedSpan.addLabels(labels); } - return cb(span) + return promise .then((res) => { - if (!span.outcome || span.outcome === 'unknown') { - span.outcome = 'success'; + if (!targetedSpan.outcome || targetedSpan.outcome === 'unknown') { + targetedSpan.outcome = 'success'; } return res; }) .catch((err) => { - if (!span.outcome || span.outcome === 'unknown') { - span.outcome = 'failure'; + if (!targetedSpan.outcome || targetedSpan.outcome === 'unknown') { + targetedSpan.outcome = 'failure'; } throw err; }) .finally(() => { - span.end(); + targetedSpan.end(); }); }); } diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 872ac46352cf31..92ea23347c374c 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -21,5 +21,6 @@ import 'monaco-editor/esm/vs/editor/contrib/folding/folding.js'; // Needed for f import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature +import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight export { monaco }; diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index e455f487d13843..1311eb4d7c6388 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline stdio: 'pipe' }); - if (offline) { + if (offline || !offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts index c030081e53daaf..5f3743876e0e4a 100644 --- a/packages/kbn-pm/src/utils/bazel/run.ts +++ b/packages/kbn-pm/src/utils/bazel/run.ts @@ -29,7 +29,7 @@ async function runBazelCommandWithRunner( stdio: 'pipe', }; - if (offline) { + if (offline || !offline) { bazelArgs = [...bazelArgs, '--config=offline']; } diff --git a/packages/kbn-test/src/jest/utils/router_helpers.tsx b/packages/kbn-test/src/jest/utils/router_helpers.tsx index e2245440274d19..85ef27488a4ce9 100644 --- a/packages/kbn-test/src/jest/utils/router_helpers.tsx +++ b/packages/kbn-test/src/jest/utils/router_helpers.tsx @@ -8,18 +8,39 @@ import React, { Component, ComponentType } from 'react'; import { MemoryRouter, Route, withRouter } from 'react-router-dom'; -import * as H from 'history'; +import { History, LocationDescriptor } from 'history'; -export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: number = 0) => ( - WrappedComponent: ComponentType -) => (props: any) => ( +const stringifyPath = (path: LocationDescriptor): string => { + if (typeof path === 'string') { + return path; + } + + return path.pathname || '/'; +}; + +const locationDescriptorToRoutePath = ( + paths: LocationDescriptor | LocationDescriptor[] +): string | string[] => { + if (Array.isArray(paths)) { + return paths.map((path: LocationDescriptor) => { + return stringifyPath(path); + }); + } + + return stringifyPath(paths); +}; + +export const WithMemoryRouter = ( + initialEntries: LocationDescriptor[] = ['/'], + initialIndex: number = 0 +) => (WrappedComponent: ComponentType) => (props: any) => ( ); export const WithRoute = ( - componentRoutePath: string | string[] = '/', + componentRoutePath: LocationDescriptor | LocationDescriptor[] = ['/'], onRouter = (router: any) => {} ) => (WrappedComponent: ComponentType) => { // Create a class component that will catch the router @@ -40,16 +61,16 @@ export const WithRoute = ( return (props: any) => ( } /> ); }; interface Router { - history: Partial; + history: Partial; route: { - location: H.Location; + location: LocationDescriptor; }; } diff --git a/packages/kbn-test/src/jest/utils/testbed/types.ts b/packages/kbn-test/src/jest/utils/testbed/types.ts index fdc000215c4f19..bba504951c0bc6 100644 --- a/packages/kbn-test/src/jest/utils/testbed/types.ts +++ b/packages/kbn-test/src/jest/utils/testbed/types.ts @@ -8,6 +8,7 @@ import { Store } from 'redux'; import { ReactWrapper } from 'enzyme'; +import { LocationDescriptor } from 'history'; export type SetupFunc = (props?: any) => TestBed | Promise>; @@ -161,11 +162,11 @@ export interface MemoryRouterConfig { /** Flag to add or not the `MemoryRouter`. If set to `false`, there won't be any router and the component won't be wrapped on a ``. */ wrapComponent?: boolean; /** The React Router **initial entries** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ - initialEntries?: string[]; + initialEntries?: LocationDescriptor[]; /** The React Router **initial index** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */ initialIndex?: number; /** The route **path** for the mounted component (defaults to `"/"`) */ - componentRoutePath?: string | string[]; + componentRoutePath?: LocationDescriptor | LocationDescriptor[]; /** A callBack that will be called with the React Router instance once mounted */ onRouter?: (router: any) => void; } diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts new file mode 100644 index 00000000000000..25651a0dd21902 --- /dev/null +++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { parseArchive } from './parse_archive'; + +jest.mock('fs/promises', () => ({ + readFile: jest.fn(), +})); + +const mockReadFile = jest.requireMock('fs/promises').readFile; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('parses archives with \\n', async () => { + mockReadFile.mockResolvedValue( + `{ + "foo": "abc" + }\n\n{ + "foo": "xyz" + }` + ); + + const archive = await parseArchive('mock'); + expect(archive).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "abc", + }, + Object { + "foo": "xyz", + }, + ] + `); +}); + +it('parses archives with \\r\\n', async () => { + mockReadFile.mockResolvedValue( + `{ + "foo": "123" + }\r\n\r\n{ + "foo": "456" + }` + ); + + const archive = await parseArchive('mock'); + expect(archive).toMatchInlineSnapshot(` + Array [ + Object { + "foo": "123", + }, + Object { + "foo": "456", + }, + ] + `); +}); diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts new file mode 100644 index 00000000000000..b6b85ba521525b --- /dev/null +++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 Fs from 'fs/promises'; + +export interface SavedObject { + id: string; + type: string; + [key: string]: unknown; +} + +export async function parseArchive(path: string): Promise { + return (await Fs.readFile(path, 'utf-8')) + .split(/\r?\n\r?\n/) + .filter((line) => !!line) + .map((line) => JSON.parse(line)); +} diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts index 5fd30929fecf68..4adae7d1cd031e 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts @@ -16,25 +16,12 @@ import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@k import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; +import { parseArchive } from './import_export/parse_archive'; interface ImportApiResponse { success: boolean; [key: string]: unknown; } - -interface SavedObject { - id: string; - type: string; - [key: string]: unknown; -} - -async function parseArchive(path: string): Promise { - return (await Fs.readFile(path, 'utf-8')) - .split('\n\n') - .filter((line) => !!line) - .map((line) => JSON.parse(line)); -} - export class KbnClientImportExport { constructor( public readonly log: ToolingLog, @@ -48,7 +35,12 @@ export class KbnClientImportExport { path = `${path}.json`; } - const absolutePath = Path.resolve(this.baseDir, path); + return Path.resolve(this.baseDir, path); + } + + private resolveAndValidatePath(path: string) { + const absolutePath = this.resolvePath(path); + if (!existsSync(absolutePath)) { throw new Error( `unable to resolve path [${path}] to import/export, resolved relative to [${this.baseDir}]` @@ -59,7 +51,7 @@ export class KbnClientImportExport { } async load(path: string, options?: { space?: string }) { - const src = this.resolvePath(path); + const src = this.resolveAndValidatePath(path); this.log.debug('resolved import for', path, 'to', src); const objects = await parseArchive(src); @@ -94,7 +86,7 @@ export class KbnClientImportExport { } async unload(path: string, options?: { space?: string }) { - const src = this.resolvePath(path); + const src = this.resolveAndValidatePath(path); this.log.debug('unloading docs from archive at', src); const objects = await parseArchive(src); @@ -143,6 +135,7 @@ export class KbnClientImportExport { }) .join('\n\n'); + await Fs.mkdir(Path.dirname(dest), { recursive: true }); await Fs.writeFile(dest, fileContents, 'utf-8'); this.log.success('Exported', objects.length, 'saved objects to', dest); diff --git a/packages/kbn-tinymath/grammar/grammar.peggy b/packages/kbn-tinymath/grammar/grammar.peggy index cbcb0b91bfea90..1c6f8c3334c234 100644 --- a/packages/kbn-tinymath/grammar/grammar.peggy +++ b/packages/kbn-tinymath/grammar/grammar.peggy @@ -1,16 +1,16 @@ // tinymath parsing grammar { - function simpleLocation (location) { - // Returns an object representing the position of the function within the expression, - // demarcated by the position of its first character and last character. We calculate these values - // using the offset because the expression could span multiple lines, and we don't want to deal - // with column and line values. - return { - min: location.start.offset, - max: location.end.offset + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } } - } } start @@ -74,26 +74,34 @@ Expression = AddSubtract AddSubtract - = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)* _ { - return rest.reduce((acc, curr) => ({ + = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)+ _ { + const topLevel = rest.reduce((acc, curr) => ({ type: 'function', name: curr[0] === '+' ? 'add' : 'subtract', args: [acc, curr[1]], - location: simpleLocation(location()), - text: text() - }), left) + }), left); + if (typeof topLevel === 'object') { + topLevel.location = simpleLocation(location()); + topLevel.text = text(); + } + return topLevel; } + / MultiplyDivide MultiplyDivide = _ left:Factor rest:(('*' / '/') Factor)* _ { - return rest.reduce((acc, curr) => ({ + const topLevel = rest.reduce((acc, curr) => ({ type: 'function', name: curr[0] === '*' ? 'multiply' : 'divide', args: [acc, curr[1]], - location: simpleLocation(location()), - text: text() - }), left) + }), left); + if (typeof topLevel === 'object') { + topLevel.location = simpleLocation(location()); + topLevel.text = text(); + } + return topLevel; } + / Factor Factor = Group diff --git a/packages/kbn-tinymath/index.d.ts b/packages/kbn-tinymath/index.d.ts index c3c32a59fa15ad..8e15d86c88fc84 100644 --- a/packages/kbn-tinymath/index.d.ts +++ b/packages/kbn-tinymath/index.d.ts @@ -24,9 +24,11 @@ export interface TinymathLocation { export interface TinymathFunction { type: 'function'; name: string; - text: string; args: TinymathAST[]; - location: TinymathLocation; + // Location is not guaranteed because PEG grammars are not left-recursive + location?: TinymathLocation; + // Text is not guaranteed because PEG grammars are not left-recursive + text?: string; } export interface TinymathVariable { diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js index bf1c7a9dbc5fb5..bbc8503684fd40 100644 --- a/packages/kbn-tinymath/test/library.test.js +++ b/packages/kbn-tinymath/test/library.test.js @@ -41,6 +41,35 @@ describe('Parser', () => { }); }); + describe('Math', () => { + it('converts basic symbols into left-to-right pairs', () => { + expect(parse('a + b + c - d')).toEqual({ + args: [ + { + name: 'add', + type: 'function', + args: [ + { + name: 'add', + type: 'function', + args: [ + expect.objectContaining({ location: { min: 0, max: 2 } }), + expect.objectContaining({ location: { min: 3, max: 6 } }), + ], + }, + expect.objectContaining({ location: { min: 7, max: 10 } }), + ], + }, + expect.objectContaining({ location: { min: 11, max: 13 } }), + ], + name: 'subtract', + type: 'function', + text: 'a + b + c - d', + location: { min: 0, max: 13 }, + }); + }); + }); + describe('Variables', () => { it('strings', () => { expect(parse('f')).toEqual(variableEqual('f')); @@ -263,6 +292,8 @@ describe('Evaluate', () => { expect(evaluate('5/20')).toEqual(0.25); expect(evaluate('1 + 1 + 2 + 3 + 12')).toEqual(19); expect(evaluate('100 / 10 / 10')).toEqual(1); + expect(evaluate('0 * 1 - 100 / 10 / 10')).toEqual(-1); + expect(evaluate('100 / (10 / 10)')).toEqual(100); }); it('equations with functions', () => { diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index 2683dd41d2bb32..dbad6bc8ba19c9 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -24,7 +24,7 @@ function processCommand(command, options) { const logger = new Logger(settings); - logWarnings(settings, logger); + logWarnings(logger); install(settings, logger); } diff --git a/src/cli_plugin/remove/index.js b/src/cli_plugin/remove/index.js index 329f506520c245..3f571e605028f3 100644 --- a/src/cli_plugin/remove/index.js +++ b/src/cli_plugin/remove/index.js @@ -24,7 +24,7 @@ function processCommand(command, options) { const logger = new Logger(settings); - logWarnings(settings, logger); + logWarnings(logger); remove(settings, logger); } diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 55c18e49cba131..2543d22ee6d31b 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -96,7 +96,7 @@ describe('AppRouter', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -108,7 +108,7 @@ describe('AppRouter', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app2 html:
App 2
" @@ -122,7 +122,7 @@ describe('AppRouter', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -134,7 +134,7 @@ describe('AppRouter', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -146,7 +146,7 @@ describe('AppRouter', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /app/app1 html: App 1
" @@ -160,7 +160,7 @@ describe('AppRouter', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" @@ -172,7 +172,7 @@ describe('AppRouter', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-b/path html:
Chromeless B
" @@ -184,7 +184,7 @@ describe('AppRouter', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
+ "
basename: /chromeless-a/path html:
Chromeless A
" diff --git a/src/core/public/application/ui/app_container.scss b/src/core/public/application/ui/app_container.scss index 4f8fec10a97e15..d30db740505d18 100644 --- a/src/core/public/application/ui/app_container.scss +++ b/src/core/public/application/ui/app_container.scss @@ -1,5 +1,5 @@ .appContainer__loading { - position: fixed; + position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 02d321095b3735..0312c707e10498 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import './app_container.scss'; import React, { Fragment, @@ -16,11 +17,12 @@ import React, { } from 'react'; import { EuiLoadingElastic } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import type { MountPoint } from '../../types'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; import { ScopedHistory } from '../scoped_history'; -import './app_container.scss'; +import { APP_WRAPPER_CLASS } from '../../../utils'; interface Props { /** Path application is mounted on without the global basePath */ @@ -107,12 +109,16 @@ export const AppContainer: FunctionComponent = ({ return ( {appNotFound && } - {showSpinner && ( -
- -
+ {showSpinner && !appNotFound && ( + )} -
+
); }; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index d4ab8f624f7111..06277d9351922c 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -142,7 +142,7 @@ export class DocLinksService { dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`, indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`, - indexTemplates: `${ELASTICSEARCH_DOCS}indices-templates.html`, + indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, mappingAnalyzer: `${ELASTICSEARCH_DOCS}analyzer.html`, mappingCoerce: `${ELASTICSEARCH_DOCS}coerce.html`, @@ -253,7 +253,7 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, }, alerting: { - guide: `${KIBANA_DOCS}alert-management.html`, + guide: `${KIBANA_DOCS}create-and-manage-rules.html`, actionTypes: `${KIBANA_DOCS}action-types.html`, emailAction: `${KIBANA_DOCS}email-action-type.html`, emailActionConfig: `${KIBANA_DOCS}email-action-type.html`, @@ -265,7 +265,7 @@ export class DocLinksService { 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`, + setupPrerequisites: `${KIBANA_DOCS}alerting-setup.html#alerting-prerequisites`, slackAction: `${KIBANA_DOCS}slack-action-type.html#configuring-slack`, teamsAction: `${KIBANA_DOCS}teams-action-type.html#configuring-teams`, }, diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 3a748f3ceb6fd3..4bd6afe90d3429 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -35,27 +35,12 @@ position: relative; // This is temporary for apps that relied on this being present on `.application` } -// TODO: This is problematic because it doesn't stay in line with EUI: -// adapted from euiHeaderAffordForFixed as we need to handle the top banner @mixin kbnAffordForHeader($headerHeight) { - padding-top: $headerHeight; + @include euiHeaderAffordForFixed($headerHeight); #app-fixed-viewport { top: $headerHeight; } - - .euiFlyout, - .euiCollapsibleNav { - top: $headerHeight; - height: calc(100% - #{$headerHeight}); - } - - @include euiBreakpoint('m', 'l', 'xl') { - .euiPageSideBar--sticky { - max-height: calc(100vh - #{$headerHeight}); - top: #{$headerHeight}; - } - } } .kbnBody { diff --git a/src/core/server/http/lifecycle/on_pre_routing.ts b/src/core/server/http/lifecycle/on_pre_routing.ts index dbd00df13707b3..be0af8c118627f 100644 --- a/src/core/server/http/lifecycle/on_pre_routing.ts +++ b/src/core/server/http/lifecycle/on_pre_routing.ts @@ -102,24 +102,7 @@ export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) { appState.rewrittenUrl = appState.rewrittenUrl ?? request.url; const { url } = result; - - // TODO: Remove once we upgrade to Node.js 12! - // - // Warning: The following for-loop took 10 days to write, and is a hack - // to force V8 to make a copy of the string in memory. - // - // The reason why we need this is because of what appears to be a bug - // in V8 that caused some URL paths to not be routed correctly once - // `request.setUrl` was called with the path. - // - // The details can be seen in this discussion on Twitter: - // https://twitter.com/wa7son/status/1319992632366518277 - let urlCopy = ''; - for (let i = 0; i < url.length; i++) { - urlCopy += url[i]; - } - - request.setUrl(urlCopy); + request.setUrl(url); // We should update raw request as well since it can be proxied to the old platform request.raw.req.url = url; diff --git a/src/core/server/rendering/views/styles.tsx b/src/core/server/rendering/views/styles.tsx index 105f94df9218fc..fbeab4fb4388fa 100644 --- a/src/core/server/rendering/views/styles.tsx +++ b/src/core/server/rendering/views/styles.tsx @@ -52,7 +52,6 @@ const InlineStyles: FC<{ darkMode: boolean }> = ({ darkMode }) => { .kbnWelcomeView { line-height: 1.5; - background-color: ${darkMode ? '#1D1E24' : '#FFF'}; height: 100%; display: -webkit-box; display: -webkit-flex; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip deleted file mode 100644 index a92211c16c5593..00000000000000 Binary files a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_so_with_multiple_namespaces.zip and /dev/null differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip index c6c89ac2879b2e..44f2fc9ba19eb5 100644 Binary files a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.0_with_corrupted_so.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip new file mode 100644 index 00000000000000..e4dce85f15e38e Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.13.2_so_with_multiple_namespaces.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts index 91d86353b48fac..83d97555a47987 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -53,8 +53,7 @@ function createRoot() { ); } -// FAILING: https://github.com/elastic/kibana/issues/98352 -describe.skip('migration v2', () => { +describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; @@ -78,7 +77,7 @@ describe.skip('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: 'trial', + license: 'basic', // original SO: // { // _index: '.kibana_7.13.0_001', diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 0f4085f6186be4..759a9572d733b2 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -89,7 +89,7 @@ function createRoot() { } // FAILING: https://github.com/elastic/kibana/issues/98351 -describe.skip('migration v2', () => { +describe('migration v2', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let root: Root; @@ -114,7 +114,7 @@ describe.skip('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: 'trial', + license: 'basic', // original SO: // [ // { id: 'foo:1', type: 'foo', foo: { name: 'Foo 1 default' } }, @@ -133,7 +133,7 @@ describe.skip('migration v2', () => { // namespace: 'spacex', // }, // ]; - dataArchive: Path.join(__dirname, 'archives', '7.13.0_so_with_multiple_namespaces.zip'), + dataArchive: Path.join(__dirname, 'archives', '7.13.2_so_with_multiple_namespaces.zip'), }, }, }); diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts index 1cfa9d862e6b95..60f1f7eb0955c0 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.test.ts @@ -72,6 +72,22 @@ const lensXYSeriesB = ({ visualization: { preferredSeriesType: 'seriesB', }, + datasourceStates: { + indexpattern: { + layers: { + first: { + columns: { + first: { + operationType: 'terms', + }, + second: { + operationType: 'formula', + }, + }, + }, + }, + }, + }, }, }, }, @@ -144,6 +160,7 @@ describe('dashboard telemetry', () => { expect(collectorData.lensByValue.a).toBe(3); expect(collectorData.lensByValue.seriesA).toBe(2); expect(collectorData.lensByValue.seriesB).toBe(1); + expect(collectorData.lensByValue.formula).toBe(1); }); it('handles misshapen lens panels', () => { diff --git a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts index 912dc04d16d092..fb1ddff469f578 100644 --- a/src/plugins/dashboard/server/usage/dashboard_telemetry.ts +++ b/src/plugins/dashboard/server/usage/dashboard_telemetry.ts @@ -27,6 +27,16 @@ interface LensPanel extends SavedDashboardPanel730ToLatest { visualization?: { preferredSeriesType?: string; }; + datasourceStates?: { + indexpattern?: { + layers: Record< + string, + { + columns: Record; + } + >; + }; + }; }; }; }; @@ -109,6 +119,19 @@ export const collectByValueLensInfo: DashboardCollectorFunction = (panels, colle } collectorData.lensByValue[type] = collectorData.lensByValue[type] + 1; + + const hasFormula = Object.values( + lensPanel.embeddableConfig.attributes.state?.datasourceStates?.indexpattern?.layers || {} + ).some((layer) => + Object.values(layer.columns).some((column) => column.operationType === 'formula') + ); + + if (hasFormula && !collectorData.lensByValue.formula) { + collectorData.lensByValue.formula = 0; + } + if (hasFormula) { + collectorData.lensByValue.formula++; + } } } }; diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts index 72551f4b7b236f..9ea9919e757de8 100644 --- a/src/plugins/data/common/field_formats/converters/duration.test.ts +++ b/src/plugins/data/common/field_formats/converters/duration.test.ts @@ -130,11 +130,11 @@ describe('Duration Format', () => { fixtures: [ { input: -60, - output: '-60 Seconds', + output: '-60 seconds', }, { input: -32.333, - output: '-32 Seconds', + output: '-32 seconds', }, ], }); @@ -147,15 +147,15 @@ describe('Duration Format', () => { fixtures: [ { input: 1988, - output: '0.00 Milliseconds', + output: '0.00 milliseconds', }, { input: 658, - output: '0.00 Milliseconds', + output: '0.00 milliseconds', }, { input: 3857, - output: '0.00 Milliseconds', + output: '0.00 milliseconds', }, ], }); @@ -168,15 +168,15 @@ describe('Duration Format', () => { fixtures: [ { input: 1988, - output: '1.99 Milliseconds', + output: '1.99 milliseconds', }, { input: 658, - output: '0.66 Milliseconds', + output: '0.66 milliseconds', }, { input: 3857, - output: '3.86 Milliseconds', + output: '3.86 milliseconds', }, ], }); @@ -189,19 +189,19 @@ describe('Duration Format', () => { fixtures: [ { input: 1988, - output: '2.0 Milliseconds', + output: '2.0 milliseconds', }, { input: 0, - output: '0.0 Milliseconds', + output: '0.0 milliseconds', }, { input: 658, - output: '0.7 Milliseconds', + output: '0.7 milliseconds', }, { input: 3857, - output: '3.9 Milliseconds', + output: '3.9 milliseconds', }, ], }); @@ -214,15 +214,15 @@ describe('Duration Format', () => { fixtures: [ { input: 600, - output: '10 Minutes', + output: '10 minutes', }, { input: 30, - output: '30 Seconds', + output: '30 seconds', }, { input: 3000, - output: '50 Minutes', + output: '50 minutes', }, ], }); diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index c9a7091db84716..71ac022ba5e5cb 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -263,7 +263,7 @@ export class DurationFormat extends FieldFormat { const precise = human || humanPrecise ? formatted : formatted.toFixed(outputPrecision); const type = outputFormats.find(({ method }) => method === outputFormat); - const unitText = useShortSuffix ? type?.shortText : type?.text; + const unitText = useShortSuffix ? type?.shortText : type?.text.toLowerCase(); const suffix = showSuffix && unitText && !human ? `${includeSpace}${unitText}` : ''; @@ -294,7 +294,7 @@ function formatDuration( const getUnitText = (method: string) => { const type = outputFormats.find(({ method: methodT }) => method === methodT); - return useShortSuffix ? type?.shortText : type?.text; + return useShortSuffix ? type?.shortText : type?.text.toLowerCase(); }; for (let i = 0; i < units.length; i++) { diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts index 108b48f9ea77ea..5d191a94d4c8d1 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.test.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.test.ts @@ -24,6 +24,9 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[0].functions[0]).toHaveProperty('name', 'kibanaFilter'); expect(actual[0].functions[0].arguments).toMatchInlineSnapshot(` Object { + "disabled": Array [ + false, + ], "negate": Array [ false, ], @@ -35,6 +38,9 @@ describe('interpreter/functions#filtersToAst', () => { expect(actual[1].functions[0]).toHaveProperty('name', 'kibanaFilter'); expect(actual[1].functions[0].arguments).toMatchInlineSnapshot(` Object { + "disabled": Array [ + false, + ], "negate": Array [ true, ], diff --git a/src/plugins/data/common/search/expressions/filters_to_ast.ts b/src/plugins/data/common/search/expressions/filters_to_ast.ts index a4dd959caecf6f..edcf884b3ed312 100644 --- a/src/plugins/data/common/search/expressions/filters_to_ast.ts +++ b/src/plugins/data/common/search/expressions/filters_to_ast.ts @@ -17,6 +17,7 @@ export const filtersToAst = (filters: Filter[] | Filter) => { buildExpressionFunction('kibanaFilter', { query: JSON.stringify(restOfFilter), negate: filter.meta.negate, + disabled: filter.meta.disabled, }), ]); }); diff --git a/src/plugins/data/common/search/expressions/kibana_filter.ts b/src/plugins/data/common/search/expressions/kibana_filter.ts index 6d6f70fa8d1d6b..c94a3763ee084e 100644 --- a/src/plugins/data/common/search/expressions/kibana_filter.ts +++ b/src/plugins/data/common/search/expressions/kibana_filter.ts @@ -13,6 +13,7 @@ import { KibanaFilter } from './kibana_context_type'; interface Arguments { query: string; negate?: boolean; + disabled?: boolean; } export type ExpressionFunctionKibanaFilter = ExpressionFunctionDefinition< @@ -45,6 +46,13 @@ export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { defaultMessage: 'Should the filter be negated', }), }, + disabled: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.functions.kibanaFilter.disabled.help', { + defaultMessage: 'Should the filter be disabled', + }), + }, }, fn(input, args) { @@ -53,7 +61,7 @@ export const kibanaFilterFunction: ExpressionFunctionKibanaFilter = { meta: { negate: args.negate || false, alias: '', - disabled: false, + disabled: args.disabled || false, }, ...JSON.parse(args.query), }; diff --git a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx index 1fc8edcb4d0659..810be94ce24b06 100644 --- a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx @@ -51,5 +51,6 @@ export function createDiscoverGridDirective(reactDirective: any) { ['settings', { watchDepth: 'reference' }], ['showTimeCol', { watchDepth: 'value' }], ['sort', { watchDepth: 'value' }], + ['className', { watchDepth: 'value' }], ]); } diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx new file mode 100644 index 00000000000000..19913ed6de8704 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useRef, useEffect } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { IScope } from 'angular'; +import { getServices } from '../../../kibana_services'; +import { DocTableLegacyProps, injectAngularElement } from './create_doc_table_react'; + +type AngularEmbeddableScope = IScope & { renderProps?: DocTableEmbeddableProps }; + +export interface DocTableEmbeddableProps extends Partial { + refs: HTMLElement; +} + +function getRenderFn(domNode: Element, props: DocTableEmbeddableProps) { + const directive = { + template: ``, + }; + + return async () => { + try { + const injector = await getServices().getEmbeddableInjector(); + return await injectAngularElement(domNode, directive.template, props, injector); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } + }; +} + +export function DiscoverDocTableEmbeddable(props: DocTableEmbeddableProps) { + return ( + + + + ); +} + +function DocTableLegacyInner(renderProps: DocTableEmbeddableProps) { + const scope = useRef(); + + useEffect(() => { + if (renderProps.refs && !scope.current) { + const fn = getRenderFn(renderProps.refs, renderProps); + fn().then((newScope) => { + scope.current = newScope; + }); + } else if (scope?.current) { + scope.current.renderProps = { ...renderProps }; + scope.current.$applyAsync(); + } + }, [renderProps]); + + useEffect(() => { + return () => { + scope.current?.$destroy(); + }; + }, []); + return ; +} diff --git a/src/plugins/discover/public/application/angular/doc_table/index.ts b/src/plugins/discover/public/application/angular/doc_table/index.ts index 2aaf5a8bda7b69..3a8f170f8680d5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.ts +++ b/src/plugins/discover/public/application/angular/doc_table/index.ts @@ -9,3 +9,4 @@ export { createDocTableDirective } from './doc_table'; export { getSort, getSortArray } from './lib/get_sort'; export { getSortForSearchSource } from './lib/get_sort_for_search_source'; +export { getDefaultSort } from './lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 65a6ee80564e9f..f1c56b7a57195b 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -51,6 +51,10 @@ export interface DiscoverGridProps { * Determines which element labels the grid for ARIA */ ariaLabelledBy: string; + /** + * Optional class name to apply + */ + className?: string; /** * Determines which columns are displayed */ @@ -175,6 +179,7 @@ export const DiscoverGrid = ({ isSortEnabled = true, isPaginationEnabled = true, controlColumnIds = ['openDetails', 'select'], + className, }: DiscoverGridProps) => { const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -284,6 +289,7 @@ export const DiscoverGrid = ({ ), [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled] ); + const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const columnsVisibility = useMemo( () => ({ @@ -368,6 +374,7 @@ export const DiscoverGrid = ({ data-title={searchTitle} data-description={searchDescription} data-document-number={displayedRows.length} + className={className} > { + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); + expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ _id: '2' })); + component.setProps({ ...props, hit: props.hits[1] }); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' }); + expect(props.setExpandedDoc).toHaveBeenCalledWith(expect.objectContaining({ _id: '1' })); + }); + + it('should not navigate with keypresses when already at the border of documents', () => { + const props = getProps(); + const component = mountWithIntl(); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowLeft' }); + expect(props.setExpandedDoc).not.toHaveBeenCalled(); + component.setProps({ ...props, hit: props.hits[props.hits.length - 1] }); + findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); + expect(props.setExpandedDoc).not.toHaveBeenCalled(); + }); }); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index 3894127891041c..aaae9afe6531a5 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -21,6 +21,7 @@ import { EuiPortal, EuiPagination, EuiHideFor, + keys, } from '@elastic/eui'; import { DocViewer } from '../doc_viewer/doc_viewer'; import { IndexPattern } from '../../../kibana_services'; @@ -87,9 +88,25 @@ export function DiscoverGridFlyout({ [hits, setExpandedDoc] ); + const onKeyDown = useCallback( + (ev: React.KeyboardEvent) => { + if (ev.key === keys.ARROW_LEFT || ev.key === keys.ARROW_RIGHT) { + ev.preventDefault(); + ev.stopPropagation(); + setPage(activePage + (ev.key === keys.ARROW_RIGHT ? 1 : -1)); + } + }, + [activePage, setPage] + ); + return ( - + { settings?: DiscoverGridSettings; description?: string; - sort?: SortOrder[]; sharedItemTitle?: string; inspectorAdapters?: Adapters; - setSortOrder?: (sortPair: SortOrder[]) => void; - setColumns?: (columns: string[]) => void; - removeColumn?: (column: string) => void; - addColumn?: (column: string) => void; - moveColumn?: (column: string, index: number) => void; + filter?: (field: IFieldType, value: string[], operator: string) => void; hits?: ElasticSearchHit[]; - indexPattern?: IndexPattern; totalHitCount?: number; - isLoading?: boolean; - showTimeCol?: boolean; - useNewFieldsApi?: boolean; + onMoveColumn?: (column: string, index: number) => void; } interface SearchEmbeddableConfig { - $rootScope: ng.IRootScopeService; - $compile: ng.ICompileService; savedSearch: SavedSearch; editUrl: string; editPath: string; @@ -77,17 +66,13 @@ interface SearchEmbeddableConfig { services: DiscoverServices; } -export class SearchEmbeddable +export class SavedSearchEmbeddable extends Embeddable implements ISearchEmbeddable { private readonly savedSearch: SavedSearch; - private $rootScope: ng.IRootScopeService; - private $compile: ng.ICompileService; private inspectorAdapters: Adapters; - private searchScope?: SearchScope; private panelTitle: string = ''; - private filtersSearchSource?: ISearchSource; - private searchInstance?: JQLite; + private filtersSearchSource!: ISearchSource; private subscription?: Subscription; public readonly type = SEARCH_EMBEDDABLE_TYPE; private filterManager: FilterManager; @@ -98,11 +83,12 @@ export class SearchEmbeddable private prevFilters?: Filter[]; private prevQuery?: Query; private prevSearchSessionId?: string; + private searchProps?: SearchProps; + + private node?: HTMLElement; constructor( { - $rootScope, - $compile, savedSearch, editUrl, editPath, @@ -130,164 +116,24 @@ export class SearchEmbeddable this.services = services; this.filterManager = filterManager; this.savedSearch = savedSearch; - this.$rootScope = $rootScope; - this.$compile = $compile; this.inspectorAdapters = { requests: new RequestAdapter(), }; - this.initializeSearchScope(); + this.initializeSearchEmbeddableProps(); this.subscription = this.getUpdated$().subscribe(() => { this.panelTitle = this.output.title || ''; - if (this.searchScope) { - this.pushContainerStateParamsToScope(this.searchScope); + if (this.searchProps) { + this.pushContainerStateParamsToProps(this.searchProps); } }); } - public getInspectorAdapters() { - return this.inspectorAdapters; - } - - public getSavedSearch() { - return this.savedSearch; - } - - /** - * - * @param {Element} domNode - */ - public render(domNode: HTMLElement) { - if (!this.searchScope) { - throw new Error('Search scope not defined'); - } - this.searchInstance = this.$compile( - this.services.uiSettings.get('doc_table:legacy') ? searchTemplate : searchTemplateGrid - )(this.searchScope); - const rootNode = angular.element(domNode); - rootNode.append(this.searchInstance); - - this.pushContainerStateParamsToScope(this.searchScope); - } - - public destroy() { - super.destroy(); - this.savedSearch.destroy(); - if (this.searchInstance) { - this.searchInstance.remove(); - } - if (this.searchScope) { - this.searchScope.$destroy(); - delete this.searchScope; - } - if (this.subscription) { - this.subscription.unsubscribe(); - } - - if (this.abortController) this.abortController.abort(); - } - - private initializeSearchScope() { - const searchScope: SearchScope = (this.searchScope = this.$rootScope.$new()); - - searchScope.description = this.savedSearch.description; - searchScope.inspectorAdapters = this.inspectorAdapters; - - const { searchSource } = this.savedSearch; - const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; - - if (!this.savedSearch.sort || !this.savedSearch.sort.length) { - this.savedSearch.sort = getDefaultSort( - indexPattern, - getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') - ); - } - - const timeRangeSearchSource = searchSource.create(); - timeRangeSearchSource.setField('filter', () => { - if (!this.searchScope || !this.input.timeRange) return; - return this.services.timefilter.createFilter(indexPattern, this.input.timeRange); - }); - - this.filtersSearchSource = searchSource.create(); - this.filtersSearchSource.setParent(timeRangeSearchSource); - - searchSource.setParent(this.filtersSearchSource); - - this.pushContainerStateParamsToScope(searchScope); - - searchScope.setSortOrder = (sort) => { - this.updateInput({ sort }); - }; - - searchScope.isLoading = true; - - const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); - searchScope.useNewFieldsApi = useNewFieldsApi; - - searchScope.addColumn = (columnName: string) => { - if (!searchScope.columns) { - return; - } - const columns = columnActions.addColumn(searchScope.columns, columnName, useNewFieldsApi); - this.updateInput({ columns }); - }; - - searchScope.removeColumn = (columnName: string) => { - if (!searchScope.columns) { - return; - } - const columns = columnActions.removeColumn(searchScope.columns, columnName, useNewFieldsApi); - this.updateInput({ columns }); - }; - - searchScope.moveColumn = (columnName, newIndex: number) => { - if (!searchScope.columns) { - return; - } - const columns = columnActions.moveColumn(searchScope.columns, columnName, newIndex); - this.updateInput({ columns }); - }; - - searchScope.setColumns = (columns: string[]) => { - this.updateInput({ columns }); - }; - - if (this.savedSearch.grid) { - searchScope.settings = this.savedSearch.grid; - } - searchScope.showTimeCol = !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); - - searchScope.filter = async (field, value, operator) => { - let filters = esFilters.generateFilters( - this.filterManager, - field, - value, - operator, - indexPattern.id! - ); - filters = filters.map((filter) => ({ - ...filter, - $state: { store: esFilters.FilterStateStore.APP_STATE }, - })); - - await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters, - }); - }; - } - - public reload() { - if (this.searchScope) - this.pushContainerStateParamsToScope(this.searchScope, { forceFetch: true }); - } - private fetch = async () => { const searchSessionId = this.input.searchSessionId; const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); - if (!this.searchScope) return; + if (!this.searchProps) return; const { searchSource } = this.savedSearch; @@ -299,8 +145,8 @@ export class SearchEmbeddable searchSource.setField( 'sort', getSortForSearchSource( - this.searchScope.sort, - this.searchScope.indexPattern, + this.searchProps!.sort, + this.searchProps!.indexPattern, this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ) ); @@ -310,8 +156,8 @@ export class SearchEmbeddable searchSource.setField('fields', [fields]); } else { searchSource.removeField('fields'); - if (this.searchScope.indexPattern) { - const fieldNames = this.searchScope.indexPattern.fields.map((field) => field.name); + if (this.searchProps.indexPattern) { + const fieldNames = this.searchProps.indexPattern.fields.map((field) => field.name); searchSource.setField('fieldsFromSource', fieldNames); } } @@ -319,9 +165,8 @@ export class SearchEmbeddable // Log request to inspector this.inspectorAdapters.requests!.reset(); - this.searchScope.$apply(() => { - this.searchScope!.isLoading = true; - }); + this.searchProps!.isLoading = true; + this.updateOutput({ loading: true, error: undefined }); try { @@ -344,64 +189,222 @@ export class SearchEmbeddable .toPromise(); this.updateOutput({ loading: false, error: undefined }); - // Apply the changes to the angular scope - this.searchScope.$apply(() => { - this.searchScope!.hits = resp.hits.hits; - this.searchScope!.totalHitCount = resp.hits.total as number; - this.searchScope!.isLoading = false; - }); + this.searchProps!.rows = resp.hits.hits; + this.searchProps!.totalHitCount = resp.hits.total as number; + this.searchProps!.isLoading = false; } catch (error) { this.updateOutput({ loading: false, error }); - this.searchScope.$apply(() => { - this.searchScope!.isLoading = false; - }); + + this.searchProps!.isLoading = false; } }; - private pushContainerStateParamsToScope( - searchScope: SearchScope, + private initializeSearchEmbeddableProps() { + const { searchSource } = this.savedSearch; + + const indexPattern = searchSource.getField('index'); + + if (!indexPattern) { + return; + } + + if (!this.savedSearch.sort || !this.savedSearch.sort.length) { + this.savedSearch.sort = getDefaultSort( + indexPattern, + getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') + ); + } + + const props: SearchProps = { + columns: this.savedSearch.columns, + indexPattern, + isLoading: false, + sort: getDefaultSort( + indexPattern, + getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') + ), + rows: [], + searchDescription: this.savedSearch.description, + description: this.savedSearch.description, + inspectorAdapters: this.inspectorAdapters, + searchTitle: this.savedSearch.lastSavedTitle, + services: this.services, + onAddColumn: (columnName: string) => { + if (!props.columns) { + return; + } + const updatedColumns = columnActions.addColumn(props.columns, columnName, true); + this.updateInput({ columns: updatedColumns }); + }, + onRemoveColumn: (columnName: string) => { + if (!props.columns) { + return; + } + const updatedColumns = columnActions.removeColumn(props.columns, columnName, true); + this.updateInput({ columns: updatedColumns }); + }, + onMoveColumn: (columnName: string, newIndex: number) => { + if (!props.columns) { + return; + } + const columns = columnActions.moveColumn(props.columns, columnName, newIndex); + this.updateInput({ columns }); + }, + onSetColumns: (columns: string[]) => { + this.updateInput({ columns }); + }, + onSort: (sort: string[][]) => { + const sortOrderArr: SortOrder[] = []; + sort.forEach((arr) => { + sortOrderArr.push(arr as SortOrder); + }); + this.updateInput({ sort: sortOrderArr }); + }, + sampleSize: 500, + onFilter: async (field, value, operator) => { + let filters = esFilters.generateFilters( + this.filterManager, + // @ts-expect-error + field, + value, + operator, + indexPattern.id! + ); + filters = filters.map((filter) => ({ + ...filter, + $state: { store: esFilters.FilterStateStore.APP_STATE }, + })); + + await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters, + }); + }, + useNewFieldsApi: !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false), + showTimeCol: !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + ariaLabelledBy: 'documentsAriaLabel', + }; + + const timeRangeSearchSource = searchSource.create(); + timeRangeSearchSource.setField('filter', () => { + if (!this.searchProps || !this.input.timeRange) return; + return this.services.timefilter.createFilter(indexPattern, this.input.timeRange); + }); + + this.filtersSearchSource = searchSource.create(); + this.filtersSearchSource.setParent(timeRangeSearchSource); + + searchSource.setParent(this.filtersSearchSource); + + this.pushContainerStateParamsToProps(props); + + props.isLoading = true; + + if (this.savedSearch.grid) { + props.settings = this.savedSearch.grid; + } + } + + private async pushContainerStateParamsToProps( + searchProps: SearchProps, { forceFetch = false }: { forceFetch: boolean } = { forceFetch: false } ) { const isFetchRequired = !esFilters.onlyDisabledFiltersChanged(this.input.filters, this.prevFilters) || - !_.isEqual(this.prevQuery, this.input.query) || - !_.isEqual(this.prevTimeRange, this.input.timeRange) || - !_.isEqual(searchScope.sort, this.input.sort || this.savedSearch.sort) || + !isEqual(this.prevQuery, this.input.query) || + !isEqual(this.prevTimeRange, this.input.timeRange) || + !isEqual(searchProps.sort, this.input.sort || this.savedSearch.sort) || this.prevSearchSessionId !== this.input.searchSessionId; // If there is column or sort data on the panel, that means the original columns or sort settings have // been overridden in a dashboard. - searchScope.columns = handleSourceColumnState( + searchProps.columns = handleSourceColumnState( { columns: this.input.columns || this.savedSearch.columns }, this.services.core.uiSettings ).columns; + const savedSearchSort = this.savedSearch.sort && this.savedSearch.sort.length ? this.savedSearch.sort : getDefaultSort( - this.searchScope?.indexPattern, + this.searchProps?.indexPattern, getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') ); - searchScope.sort = this.input.sort || savedSearchSort; - searchScope.sharedItemTitle = this.panelTitle; - + searchProps.sort = this.input.sort || savedSearchSort; + searchProps.sharedItemTitle = this.panelTitle; if (forceFetch || isFetchRequired) { - this.filtersSearchSource!.setField('filter', this.input.filters); - this.filtersSearchSource!.setField('query', this.input.query); + this.filtersSearchSource.setField('filter', this.input.filters); + this.filtersSearchSource.setField('query', this.input.query); if (this.input.query?.query || this.input.filters?.length) { - this.filtersSearchSource!.setField('highlightAll', true); + this.filtersSearchSource.setField('highlightAll', true); } else { - this.filtersSearchSource!.removeField('highlightAll'); + this.filtersSearchSource.removeField('highlightAll'); } this.prevFilters = this.input.filters; this.prevQuery = this.input.query; this.prevTimeRange = this.input.timeRange; this.prevSearchSessionId = this.input.searchSessionId; - this.fetch(); - } else if (this.searchScope) { - // trigger a digest cycle to make sure non-fetch relevant changes are propagated - this.searchScope.$applyAsync(); + this.searchProps = searchProps; + await this.fetch(); + } else if (this.searchProps && this.node) { + this.searchProps = searchProps; + } + + if (this.node) { + this.renderReactComponent(this.node, this.searchProps!); + } + } + + /** + * + * @param {Element} domNode + */ + public async render(domNode: HTMLElement) { + if (!this.searchProps) { + throw new Error('Search props not defined'); + } + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = domNode; + } + + private renderReactComponent(domNode: HTMLElement, searchProps: SearchProps) { + if (!this.searchProps) { + return; + } + const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY); + const props = { + searchProps, + useLegacyTable, + refs: domNode, + }; + ReactDOM.render(, domNode); + } + + public reload() { + if (this.searchProps) { + this.pushContainerStateParamsToProps(this.searchProps, { forceFetch: true }); } } + + public getSavedSearch(): SavedSearch { + return this.savedSearch; + } + + public getInspectorAdapters() { + return this.inspectorAdapters; + } + + public destroy() { + super.destroy(); + this.savedSearch.destroy(); + if (this.searchProps) { + delete this.searchProps; + } + this.subscription?.unsubscribe(); + + if (this.abortController) this.abortController.abort(); + } } diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx new file mode 100644 index 00000000000000..5b2a2635d04bdf --- /dev/null +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { DiscoverGridEmbeddable } from '../angular/create_discover_grid_directive'; +import { DiscoverDocTableEmbeddable } from '../angular/doc_table/create_doc_table_embeddable'; +import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; +import { SearchProps } from './saved_search_embeddable'; + +interface SavedSearchEmbeddableComponentProps { + searchProps: SearchProps; + useLegacyTable: boolean; + refs: HTMLElement; +} + +const DiscoverDocTableEmbeddableMemoized = React.memo(DiscoverDocTableEmbeddable); +const DiscoverGridEmbeddableMemoized = React.memo(DiscoverGridEmbeddable); + +export function SavedSearchEmbeddableComponent({ + searchProps, + useLegacyTable, + refs, +}: SavedSearchEmbeddableComponentProps) { + if (useLegacyTable) { + const docTableProps = { + ...searchProps, + refs, + }; + return ; + } + const discoverGridProps = searchProps as DiscoverGridProps; + return ; +} diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 77da138d118dd0..360844976284eb 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -18,8 +18,9 @@ import { import { TimeRange } from '../../../../data/public'; -import { SearchInput, SearchOutput, SearchEmbeddable } from './types'; +import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +import { SavedSearchEmbeddable } from './saved_search_embeddable'; interface StartServices { executeTriggerActions: UiActionsStart['executeTriggerActions']; @@ -27,7 +28,7 @@ interface StartServices { } export class SearchEmbeddableFactory - implements EmbeddableFactoryDefinition { + implements EmbeddableFactoryDefinition { public readonly type = SEARCH_EMBEDDABLE_TYPE; private $injector: auto.IInjectorService | null; private getInjector: () => Promise | null; @@ -65,14 +66,11 @@ export class SearchEmbeddableFactory savedObjectId: string, input: Partial & { id: string; timeRange: TimeRange }, parent?: Container - ): Promise => { + ): Promise => { if (!this.$injector) { this.$injector = await this.getInjector(); } - const $injector = this.$injector as auto.IInjectorService; - const $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); const filterManager = getServices().filterManager; const url = await getServices().getSavedSearchUrlById(savedObjectId); @@ -81,12 +79,12 @@ export class SearchEmbeddableFactory const savedObject = await getServices().getSavedSearchById(savedObjectId); const indexPattern = savedObject.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); - const { SearchEmbeddable: SearchEmbeddableClass } = await import('./search_embeddable'); - return new SearchEmbeddableClass( + const { SavedSearchEmbeddable: SavedSearchEmbeddableClass } = await import( + './saved_search_embeddable' + ); + return new SavedSearchEmbeddableClass( { savedSearch: savedObject, - $rootScope, - $compile, editUrl, editPath: url, filterManager, diff --git a/src/plugins/discover/public/application/embeddable/search_template.html b/src/plugins/discover/public/application/embeddable/search_template.html deleted file mode 100644 index 3e37b3645650f2..00000000000000 --- a/src/plugins/discover/public/application/embeddable/search_template.html +++ /dev/null @@ -1,21 +0,0 @@ - - diff --git a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html b/src/plugins/discover/public/application/embeddable/search_template_datagrid.html deleted file mode 100644 index 8ad7938350d9c5..00000000000000 --- a/src/plugins/discover/public/application/embeddable/search_template_datagrid.html +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 69687f75f30982..feff425cc48edd 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -834,8 +834,8 @@ describe('Execution', () => { expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual( { - name: 'foo', - value: 5, + name: ['foo'], + value: [5], } ); }); diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 6175c9e170a906..3c24a3c24e01bc 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -52,12 +52,6 @@ describe('Executor', () => { executor.registerFunction(expressionFunctions.clog); }); - test('can register all functions', () => { - const executor = new Executor(); - for (const functionDefinition of expressionFunctions.functionSpecs) - executor.registerFunction(functionDefinition); - }); - test('can retrieve all functions', () => { const executor = new Executor(); executor.registerFunction(expressionFunctions.clog); @@ -67,12 +61,24 @@ describe('Executor', () => { test('can retrieve all functions - 2', () => { const executor = new Executor(); - for (const functionDefinition of expressionFunctions.functionSpecs) + const functionSpecs = [ + expressionFunctions.clog, + expressionFunctions.font, + expressionFunctions.variableSet, + expressionFunctions.variable, + expressionFunctions.theme, + expressionFunctions.cumulativeSum, + expressionFunctions.derivative, + expressionFunctions.movingAverage, + expressionFunctions.mapColumn, + expressionFunctions.math, + ]; + for (const functionDefinition of functionSpecs) { executor.registerFunction(functionDefinition); + } const functions = executor.getFunctions(); - expect(Object.keys(functions).sort()).toEqual( - expressionFunctions.functionSpecs.map((spec) => spec.name).sort() - ); + + expect(Object.keys(functions).sort()).toEqual(functionSpecs.map((spec) => spec.name).sort()); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 1eea51a0e1ec45..a307172aff9737 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -19,7 +19,6 @@ import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; import { ExpressionAstExpression, ExpressionAstFunction } from '../ast'; import { ExpressionValueError, typeSpecs } from '../expression_types/specs'; -import { functionSpecs } from '../expression_functions/specs'; import { getByAlias } from '../util'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; @@ -85,7 +84,7 @@ export class Executor = Record { const executor = new Executor(state); for (const type of typeSpecs) executor.registerType(type); - for (const func of functionSpecs) executor.registerFunction(func); + return executor; } diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index 9408b3a4337120..20a6f9aac45674 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -6,31 +6,6 @@ * Side Public License, v 1. */ -import { clog } from './clog'; -import { font } from './font'; -import { variableSet } from './var_set'; -import { variable } from './var'; -import { AnyExpressionFunctionDefinition } from '../types'; -import { theme } from './theme'; -import { cumulativeSum } from './cumulative_sum'; -import { derivative } from './derivative'; -import { movingAverage } from './moving_average'; -import { mapColumn } from './map_column'; -import { math } from './math'; - -export const functionSpecs: AnyExpressionFunctionDefinition[] = [ - clog, - font, - variableSet, - variable, - theme, - cumulativeSum, - derivative, - movingAverage, - mapColumn, - math, -]; - export * from './clog'; export * from './font'; export * from './var_set'; @@ -39,5 +14,6 @@ export * from './theme'; export * from './cumulative_sum'; export * from './derivative'; export * from './moving_average'; +export * from './ui_setting'; export { mapColumn, MapColumnArguments } from './map_column'; export { math, MathArguments, MathInput } from './math'; diff --git a/src/plugins/expressions/common/expression_functions/specs/map_column.ts b/src/plugins/expressions/common/expression_functions/specs/map_column.ts index 7939441ff0d602..7ea96ee7fdde82 100644 --- a/src/plugins/expressions/common/expression_functions/specs/map_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/map_column.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { Observable, defer, of, zip } from 'rxjs'; +import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; -import { Datatable, getType } from '../../expression_types'; +import { Datatable, DatatableColumn, getType } from '../../expression_types'; export interface MapColumnArguments { id?: string | null; name: string; - expression?(datatable: Datatable): Observable; + expression(datatable: Datatable): Observable; copyMetaFrom?: string | null; } @@ -23,7 +23,7 @@ export const mapColumn: ExpressionFunctionDefinition< 'mapColumn', Datatable, MapColumnArguments, - Promise + Observable > = { name: 'mapColumn', aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file. @@ -80,57 +80,56 @@ export const mapColumn: ExpressionFunctionDefinition< default: null, }, }, - fn: (input, args) => { - const expression = (...params: Parameters['expression']>) => - args - .expression?.(...params) - .pipe(take(1)) - .toPromise() ?? Promise.resolve(null); + fn(input, args) { + const existingColumnIndex = input.columns.findIndex(({ id, name }) => + args.id ? id === args.id : name === args.name + ); + const id = input.columns[existingColumnIndex]?.id ?? args.id ?? args.name; - const columns = [...input.columns]; - const existingColumnIndex = columns.findIndex(({ id, name }) => { - if (args.id) { - return id === args.id; - } - return name === args.name; - }); - const columnId = - existingColumnIndex === -1 ? args.id ?? args.name : columns[existingColumnIndex].id; - - const rowPromises = input.rows.map((row) => { - return expression({ - type: 'datatable', - columns, - rows: [row], - }).then((val) => ({ - ...row, - [columnId]: val, - })); - }); + return defer(() => { + const rows$ = input.rows.length + ? zip( + ...input.rows.map((row) => + args + .expression({ + type: 'datatable', + columns: [...input.columns], + rows: [row], + }) + .pipe(map((value) => ({ ...row, [id]: value }))) + ) + ) + : of([]); - return Promise.all(rowPromises).then((rows) => { - const type = rows.length ? getType(rows[0][columnId]) : 'null'; - const newColumn = { - id: columnId, - name: args.name, - meta: { type }, - }; - if (args.copyMetaFrom) { - const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom); - newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta || {}) }; - } + return rows$.pipe( + map((rows) => { + const type = getType(rows[0]?.[id]); + const newColumn: DatatableColumn = { + id, + name: args.name, + meta: { type, params: { id: type } }, + }; + if (args.copyMetaFrom) { + const metaSourceFrom = input.columns.find( + ({ id: columnId }) => columnId === args.copyMetaFrom + ); + newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta ?? {}) }; + } - if (existingColumnIndex === -1) { - columns.push(newColumn); - } else { - columns[existingColumnIndex] = newColumn; - } + const columns = [...input.columns]; + if (existingColumnIndex === -1) { + columns.push(newColumn); + } else { + columns[existingColumnIndex] = newColumn; + } - return { - type: 'datatable', - columns, - rows, - } as Datatable; + return { + columns, + rows, + type: 'datatable', + }; + }) + ); }); }, }; diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts index f5c1f3838f66c7..bd934745fed723 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/map_column.test.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; +import { of, Observable } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { Datatable } from '../../../expression_types'; import { mapColumn, MapColumnArguments } from '../map_column'; import { emptyTable, functionWrapper, testTable } from './utils'; @@ -16,138 +17,227 @@ const pricePlusTwo = (datatable: Datatable) => of(datatable.rows[0].price + 2); describe('mapColumn', () => { const fn = functionWrapper(mapColumn); const runFn = (input: Datatable, args: MapColumnArguments) => - fn(input, args) as Promise; + fn(input, args) as Observable; + let testScheduler: TestScheduler; - it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', async () => { - const arbitraryRowIndex = 2; - const result = await runFn(testTable, { - id: 'pricePlusTwo', - name: 'pricePlusTwo', - expression: pricePlusTwo, - }); - - expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([ - ...testTable.columns, - { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } }, - ]); - expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo'); + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected)); }); - it('allows the id arg to be optional, looking up by name instead', async () => { - const result = await runFn(testTable, { name: 'name label', expression: pricePlusTwo }); - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name label'); - const arbitraryRowIndex = 4; - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(testTable.columns.length); - expect(result.columns[nameColumnIndex]).toHaveProperty('id', 'name'); - expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name label'); - expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202); - expect(result.rows[arbitraryRowIndex]).not.toHaveProperty('name label'); + it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(testTable, { + id: 'pricePlusTwo', + name: 'pricePlusTwo', + expression: pricePlusTwo, + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + ...testTable.columns, + { + id: 'pricePlusTwo', + name: 'pricePlusTwo', + meta: { type: 'number', params: { id: 'number' } }, + }, + ], + rows: expect.arrayContaining([ + expect.objectContaining({ + pricePlusTwo: expect.anything(), + }), + ]), + }), + ]); + }); }); - it('allows a duplicate name when the ids are different', async () => { - const result = await runFn(testTable, { - id: 'new', - name: 'name label', - expression: pricePlusTwo, + it('allows the id arg to be optional, looking up by name instead', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(runFn(testTable, { name: 'name label', expression: pricePlusTwo })).toBe( + '(0|)', + [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'name', + name: 'name label', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + name: 202, + }), + ]), + }), + ] + ); }); - const nameColumnIndex = result.columns.findIndex(({ id }) => id === 'new'); - const arbitraryRowIndex = 4; - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(testTable.columns.length + 1); - expect(result.columns[nameColumnIndex]).toHaveProperty('id', 'new'); - expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name label'); - expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('new', 202); }); - it('adds a column to empty tables', async () => { - const result = await runFn(emptyTable, { name: 'name', expression: pricePlusTwo }); + it('allows a duplicate name when the ids are different', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(testTable, { + id: 'new', + name: 'name label', + expression: pricePlusTwo, + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'new', + name: 'name label', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + new: 202, + }), + ]), + }), + ]); + }); + }); - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); + it('overwrites existing column with the new column if an existing column name is provided', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(runFn(testTable, { name: 'name', expression: pricePlusTwo })).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + name: 'name', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + rows: expect.arrayContaining([ + expect.objectContaining({ + name: 202, + }), + ]), + }), + ]); + }); }); - it('should assign specific id, different from name, when id arg is passed for new columns', async () => { - const result = await runFn(emptyTable, { name: 'name', id: 'myid', expression: pricePlusTwo }); + it('adds a column to empty tables', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable(runFn(emptyTable, { name: 'name', expression: pricePlusTwo })).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + expect.objectContaining({ + name: 'name', + meta: expect.objectContaining({ type: 'null' }), + }), + ], + }), + ]); + }); + }); - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('id', 'myid'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); + it('should assign specific id, different from name, when id arg is passed for copied column', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(testTable, { name: 'name', id: 'myid', expression: pricePlusTwo }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'myid', + name: 'name', + meta: expect.objectContaining({ type: 'number' }), + }), + ]), + }), + ]); + }); }); - it('should copy over the meta information from the specified column', async () => { - const result = await runFn( - { - ...testTable, - columns: [ - ...testTable.columns, - // add a new entry + it('should copy over the meta information from the specified column', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn( { - id: 'myId', - name: 'myName', - meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + ...testTable, + columns: [ + ...testTable.columns, + // add a new entry + { + id: 'myId', + name: 'myName', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + }, + ], + rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })), }, - ], - rows: testTable.rows.map((row) => ({ ...row, myId: Date.now() })), - }, - { name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo } - ); - const nameColumnIndex = result.columns.findIndex(({ name }) => name === 'name'); - - expect(result.type).toBe('datatable'); - expect(result.columns[nameColumnIndex]).toEqual({ - id: 'name', - name: 'name', - meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + { name: 'name', copyMetaFrom: 'myId', expression: pricePlusTwo } + ) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: expect.arrayContaining([ + expect.objectContaining({ + id: 'name', + name: 'name', + meta: { type: 'date', params: { id: 'number', params: { digits: 2 } } }, + }), + ]), + }), + ]); }); }); - it('should be resilient if the references column for meta information does not exists', async () => { - const result = await runFn(emptyTable, { - name: 'name', - copyMetaFrom: 'time', - expression: pricePlusTwo, + it('should be resilient if the references column for meta information does not exists', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn(emptyTable, { + name: 'name', + copyMetaFrom: 'time', + expression: pricePlusTwo, + }) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + expect.objectContaining({ + id: 'name', + name: 'name', + meta: expect.objectContaining({ type: 'null' }), + }), + ], + }), + ]); }); - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('id', 'name'); - expect(result.columns[0].meta).toHaveProperty('type', 'null'); }); - it('should correctly infer the type fromt he first row if the references column for meta information does not exists', async () => { - const result = await runFn( - { ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] }, - { name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo } - ); - - expect(result.type).toBe('datatable'); - expect(result.columns).toHaveLength(1); - expect(result.columns[0]).toHaveProperty('name', 'value'); - expect(result.columns[0]).toHaveProperty('id', 'value'); - expect(result.columns[0].meta).toHaveProperty('type', 'number'); - }); - - describe('expression', () => { - it('maps null values to the new column', async () => { - const result = await runFn(testTable, { name: 'empty' }); - const emptyColumnIndex = result.columns.findIndex(({ name }) => name === 'empty'); - const arbitraryRowIndex = 8; - - expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty'); - expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null'); - expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null); + it('should correctly infer the type fromt he first row if the references column for meta information does not exists', () => { + testScheduler.run(({ expectObservable }) => { + expectObservable( + runFn( + { ...emptyTable, rows: [...emptyTable.rows, { value: 5 }] }, + { name: 'value', copyMetaFrom: 'time', expression: pricePlusTwo } + ) + ).toBe('(0|)', [ + expect.objectContaining({ + type: 'datatable', + columns: [ + expect.objectContaining({ + id: 'value', + name: 'value', + meta: expect.objectContaining({ type: 'number' }), + }), + ], + }), + ]); }); }); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts new file mode 100644 index 00000000000000..fb2c87588a4d47 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/tests/ui_setting.test.ts @@ -0,0 +1,78 @@ +/* + * 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. + */ + +jest.mock('../../../../common'); + +import { IUiSettingsClient } from 'src/core/public'; +import { getUiSettingFn } from '../ui_setting'; + +describe('uiSetting', () => { + describe('fn', () => { + let getStartDependencies: jest.MockedFunction< + Parameters[0]['getStartDependencies'] + >; + let uiSetting: ReturnType; + let uiSettings: jest.Mocked; + + beforeEach(() => { + uiSettings = ({ + get: jest.fn(), + } as unknown) as jest.Mocked; + getStartDependencies = (jest.fn(async () => ({ + uiSettings, + })) as unknown) as typeof getStartDependencies; + + uiSetting = getUiSettingFn({ getStartDependencies }); + }); + + it('should return a value', () => { + uiSettings.get.mockReturnValueOnce('value'); + + expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).resolves.toEqual({ + type: 'ui_setting', + key: 'something', + value: 'value', + }); + }); + + it('should pass a default value', async () => { + await uiSetting.fn(null, { parameter: 'something', default: 'default' }, {} as any); + + expect(uiSettings.get).toHaveBeenCalledWith('something', 'default'); + }); + + it('should throw an error when parameter does not exist', () => { + uiSettings.get.mockImplementationOnce(() => { + throw new Error(); + }); + + expect(uiSetting.fn(null, { parameter: 'something' }, {} as any)).rejects.toEqual( + new Error('Invalid parameter "something".') + ); + }); + + it('should get a request instance on the server-side', async () => { + const request = {}; + await uiSetting.fn(null, { parameter: 'something' }, { + getKibanaRequest: () => request, + } as any); + + const [[getKibanaRequest]] = getStartDependencies.mock.calls; + + expect(getKibanaRequest()).toBe(request); + }); + + it('should throw an error if request is not provided on the server-side', async () => { + await uiSetting.fn(null, { parameter: 'something' }, {} as any); + + const [[getKibanaRequest]] = getStartDependencies.mock.calls; + + expect(getKibanaRequest).toThrow(); + }); + }); +}); diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts index 0a9f022ce89cad..cdcae61215fa42 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts @@ -9,6 +9,8 @@ import { functionWrapper } from './utils'; import { variableSet } from '../var_set'; import { ExecutionContext } from '../../../execution/types'; +import { createUnitTestExecutor } from '../../../test_helpers'; +import { first } from 'rxjs/operators'; describe('expression_functions', () => { describe('var_set', () => { @@ -32,21 +34,49 @@ describe('expression_functions', () => { }); it('updates a variable', () => { - const actual = fn(input, { name: 'test', value: 2 }, context); + const actual = fn(input, { name: ['test'], value: [2] }, context); expect(variables.test).toEqual(2); expect(actual).toEqual(input); }); it('sets a new variable', () => { - const actual = fn(input, { name: 'new', value: 3 }, context); + const actual = fn(input, { name: ['new'], value: [3] }, context); expect(variables.new).toEqual(3); expect(actual).toEqual(input); }); it('stores context if value is not set', () => { - const actual = fn(input, { name: 'test' }, context); + const actual = fn(input, { name: ['test'], value: [] }, context); expect(variables.test).toEqual(input); expect(actual).toEqual(input); }); + + it('sets multiple variables', () => { + const actual = fn(input, { name: ['new1', 'new2', 'new3'], value: [1, , 3] }, context); + expect(variables.new1).toEqual(1); + expect(variables.new2).toEqual(input); + expect(variables.new3).toEqual(3); + expect(actual).toEqual(input); + }); + + describe('running function thru executor', () => { + const executor = createUnitTestExecutor(); + executor.registerFunction(variableSet); + + it('sets the variables', async () => { + const vars = {}; + const result = await executor + .run('var_set name=test1 name=test2 value=1', 2, { variables: vars }) + .pipe(first()) + .toPromise(); + + expect(result).toEqual(2); + + expect(vars).toEqual({ + test1: 1, + test2: 2, + }); + }); + }); }); }); diff --git a/src/plugins/expressions/common/expression_functions/specs/ui_setting.ts b/src/plugins/expressions/common/expression_functions/specs/ui_setting.ts new file mode 100644 index 00000000000000..8e352e12d49c59 --- /dev/null +++ b/src/plugins/expressions/common/expression_functions/specs/ui_setting.ts @@ -0,0 +1,94 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { KibanaRequest } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { UiSetting } from '../../expression_types/specs/ui_setting'; + +interface UiSettingsClient { + get(key: string, defaultValue?: T): T | Promise; +} + +interface UiSettingStartDependencies { + uiSettings: UiSettingsClient; +} + +interface UiSettingFnArguments { + getStartDependencies(getKibanaRequest: () => KibanaRequest): Promise; +} + +export interface UiSettingArguments { + default?: unknown; + parameter: string; +} + +export type ExpressionFunctionUiSetting = ExpressionFunctionDefinition< + 'uiSetting', + unknown, + UiSettingArguments, + Promise +>; + +export function getUiSettingFn({ + getStartDependencies, +}: UiSettingFnArguments): ExpressionFunctionUiSetting { + return { + name: 'uiSetting', + help: i18n.translate('expressions.functions.uiSetting.help', { + defaultMessage: 'Returns a UI settings parameter value.', + }), + args: { + default: { + help: i18n.translate('expressions.functions.uiSetting.args.default', { + defaultMessage: 'A default value in case of the parameter is not set.', + }), + }, + parameter: { + aliases: ['_'], + help: i18n.translate('expressions.functions.uiSetting.args.parameter', { + defaultMessage: 'The parameter name.', + }), + required: true, + types: ['string'], + }, + }, + async fn(input, { default: defaultValue, parameter }, { getKibanaRequest }) { + const { uiSettings } = await getStartDependencies(() => { + const request = getKibanaRequest?.(); + if (!request) { + throw new Error( + i18n.translate('expressions.functions.uiSetting.error.kibanaRequest', { + defaultMessage: + 'A KibanaRequest is required to get UI settings on the server. ' + + 'Please provide a request object to the expression execution params.', + }) + ); + } + + return request; + }); + + try { + return { + type: 'ui_setting', + key: parameter, + value: await uiSettings.get(parameter, defaultValue), + }; + } catch { + throw new Error( + i18n.translate('expressions.functions.uiSetting.error.parameter', { + defaultMessage: 'Invalid parameter "{parameter}".', + values: { parameter }, + }) + ); + } + }, + }; +} diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts index 490c7781a01a1e..f3ac6a2ab80d4a 100644 --- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts +++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from '../types'; interface Arguments { - name: string; - value?: any; + name: string[]; + value: any[]; } export type ExpressionFunctionVarSet = ExpressionFunctionDefinition< @@ -31,12 +31,14 @@ export const variableSet: ExpressionFunctionVarSet = { types: ['string'], aliases: ['_'], required: true, + multi: true, help: i18n.translate('expressions.functions.varset.name.help', { defaultMessage: 'Specify the name of the variable.', }), }, value: { aliases: ['val'], + multi: true, help: i18n.translate('expressions.functions.varset.val.help', { defaultMessage: 'Specify the value for the variable. When unspecified, the input context is used.', @@ -45,7 +47,9 @@ export const variableSet: ExpressionFunctionVarSet = { }, fn(input, args, context) { const variables: Record = context.variables; - variables[args.name] = args.value === undefined ? input : args.value; + args.name.forEach((name, i) => { + variables[name] = args.value[i] === undefined ? input : args.value[i]; + }); return input; }, }; diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index b91e16d1804aa6..e1378a27bdfc29 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { ArgumentType } from './arguments'; -import { TypeToString } from '../types/common'; +import { TypeToString, TypeString, UnmappedTypeStrings } from '../types/common'; import { ExecutionContext } from '../execution/types'; import { ExpressionFunctionClog, @@ -47,7 +46,7 @@ export interface ExpressionFunctionDefinition< /** * Name of type of value this function outputs. */ - type?: TypeToString>; + type?: TypeString | UnmappedTypeStrings; /** * List of allowed type names for input value of this function. If this diff --git a/src/plugins/expressions/common/expression_types/specs/index.ts b/src/plugins/expressions/common/expression_types/specs/index.ts index 70427f8b337d89..c990d74672fcc4 100644 --- a/src/plugins/expressions/common/expression_types/specs/index.ts +++ b/src/plugins/expressions/common/expression_types/specs/index.ts @@ -21,6 +21,7 @@ import { shape } from './shape'; import { string } from './string'; import { style } from './style'; import { AnyExpressionTypeDefinition } from '../types'; +import { uiSetting } from './ui_setting'; export const typeSpecs: AnyExpressionTypeDefinition[] = [ boolean, @@ -37,6 +38,7 @@ export const typeSpecs: AnyExpressionTypeDefinition[] = [ shape, string, style, + uiSetting, ]; export * from './boolean'; @@ -53,3 +55,4 @@ export * from './render'; export * from './shape'; export * from './string'; export * from './style'; +export * from './ui_setting'; diff --git a/src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts b/src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts new file mode 100644 index 00000000000000..a0a3d0d396b6e6 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/specs/tests/ui_setting.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UiSetting, uiSetting } from '../ui_setting'; + +function createUiSetting(value: unknown, key = 'something'): UiSetting { + return { + key, + value, + type: 'ui_setting', + }; +} + +describe('uiSetting', () => { + describe('to', () => { + describe('render', () => { + it.each` + value | expected + ${{ a: 'b' }} | ${JSON.stringify({ a: 'b' })} + ${null} | ${''} + ${'something'} | ${'something'} + `('should render "$value" as "$expected"', ({ expected, value }) => { + expect(uiSetting.to?.render(createUiSetting(value), {})).toHaveProperty( + 'value.text', + expected + ); + }); + }); + + describe('datatable', () => { + it('should use parameter name as a datatable column', () => { + expect(uiSetting.to?.datatable(createUiSetting('value', 'column'), {})).toHaveProperty( + 'columns.0', + expect.objectContaining({ id: 'column', name: 'column' }) + ); + }); + + it.each` + value | type + ${null} | ${'null'} + ${undefined} | ${'null'} + ${'something'} | ${'string'} + ${['123']} | ${'string'} + ${123} | ${'number'} + ${[123]} | ${'number'} + ${true} | ${'boolean'} + ${{ a: 'b' }} | ${'object'} + ${[]} | ${'unknown'} + `('should determine $type type', ({ value, type }) => { + expect(uiSetting.to?.datatable(createUiSetting(value, 'column'), {})).toHaveProperty( + 'columns.0.meta.type', + type + ); + }); + + it('should put a value into a row', () => { + expect(uiSetting.to?.datatable(createUiSetting('value'), {})).toHaveProperty( + 'rows.0.something', + 'value' + ); + }); + + it('should put an array value into multiple rows', () => { + expect(uiSetting.to?.datatable(createUiSetting(['a', 'b']), {})).toHaveProperty( + 'rows', + expect.arrayContaining([ + expect.objectContaining({ something: 'a' }), + expect.objectContaining({ something: 'b' }), + ]) + ); + }); + }); + }); +}); diff --git a/src/plugins/expressions/common/expression_types/specs/ui_setting.ts b/src/plugins/expressions/common/expression_types/specs/ui_setting.ts new file mode 100644 index 00000000000000..aaea92e89a8325 --- /dev/null +++ b/src/plugins/expressions/common/expression_types/specs/ui_setting.ts @@ -0,0 +1,65 @@ +/* + * 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 { Datatable, DatatableColumnType } from './datatable'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; +import { ExpressionValueRender } from './render'; + +const name = 'ui_setting'; + +function getType(value: unknown): DatatableColumnType { + if (value == null) { + return 'null'; + } + + if (Array.isArray(value)) { + return value.length ? getType(value[0]) : 'unknown'; + } + + if (['boolean', 'number', 'object', 'string'].includes(typeof value)) { + return typeof value as DatatableColumnType; + } + + return 'unknown'; +} + +export type UiSetting = ExpressionValueBoxed<'ui_setting', { key: string; value: unknown }>; + +export const uiSetting: ExpressionTypeDefinition<'ui_setting', UiSetting> = { + name, + to: { + boolean({ value }) { + return Boolean(value); + }, + number({ value }) { + return Number(value); + }, + string({ value }) { + return String(value ?? ''); + }, + render({ value }): ExpressionValueRender<{ text: string }> { + return { + type: 'render', + as: 'text', + value: { + text: + typeof value === 'object' && value !== null + ? JSON.stringify(value) + : String(value ?? ''), + }, + }; + }, + datatable({ key, value }): Datatable { + return { + type: 'datatable', + columns: [{ id: key, name: key, meta: { type: getType(value) } }], + rows: (Array.isArray(value) ? value : [value]).map((cell) => ({ [key]: cell })), + }; + }, + }, +}; diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index d57c1748954abf..a8839c9b0d71e1 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -19,6 +19,18 @@ import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; import { PersistableStateService, SerializableState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; +import { + clog, + font, + variableSet, + variable, + theme, + cumulativeSum, + derivative, + movingAverage, + mapColumn, + math, +} from '../expression_functions'; /** * The public contract that `ExpressionsService` provides to other plugins @@ -269,7 +281,7 @@ export class ExpressionsService implements PersistableStateService { const executor = this.executor.fork(); const renderers = this.renderers; - const fork = new ExpressionsService({ executor, renderers }); + const fork = new (this.constructor as typeof ExpressionsService)({ executor, renderers }); return fork; }; @@ -318,7 +330,22 @@ export class ExpressionsService implements PersistableStateService) { + return getCommonUiSettingFn({ + async getStartDependencies() { + const [{ uiSettings }] = await getStartServices(); + + return { uiSettings }; + }, + }); +} diff --git a/src/plugins/expressions/public/plugin.test.ts b/src/plugins/expressions/public/plugin.test.ts index b83c92f5d1a96b..93353ebbb51b61 100644 --- a/src/plugins/expressions/public/plugin.test.ts +++ b/src/plugins/expressions/public/plugin.test.ts @@ -8,7 +8,7 @@ import { expressionsPluginMock } from './mocks'; import { add } from '../common/test_helpers/expression_functions/add'; -import { ExpressionsService } from '../common'; +import { ExpressionsService } from './services'; describe('ExpressionsPublicPlugin', () => { test('can instantiate from mocks', async () => { diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 2410ad87413129..37d519ac451336 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -6,9 +6,15 @@ * Side Public License, v 1. */ +import { pick } from 'lodash'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { ExpressionsService, ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; -import { setRenderersRegistry, setNotifications, setExpressionsService } from './services'; +import { ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; +import { + ExpressionsService, + setRenderersRegistry, + setNotifications, + setExpressionsService, +} from './services'; import { ReactExpressionRenderer } from './react_expression_renderer'; import { ExpressionLoader, IExpressionLoader, loader } from './loader'; import { render, ExpressionRenderHandler } from './render'; @@ -51,7 +57,7 @@ export class ExpressionsPublicPlugin implements Plugin>; name: Name; - type?: TypeToString>; + type?: TypeString | UnmappedTypeStrings; } // @public @@ -612,8 +612,8 @@ export class ExpressionsService implements PersistableStateService) => Record; diff --git a/src/plugins/expressions/public/services/expressions_services.ts b/src/plugins/expressions/public/services/expressions_services.ts new file mode 100644 index 00000000000000..388af81629c31d --- /dev/null +++ b/src/plugins/expressions/public/services/expressions_services.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 { CoreSetup } from 'src/core/public'; +import { ExpressionsService as CommonExpressionsService } from '../../common'; +import { getUiSettingFn } from '../expression_functions'; + +export class ExpressionsService extends CommonExpressionsService { + setup({ getStartServices }: Pick) { + this.registerFunction(getUiSettingFn({ getStartServices })); + + return super.setup(); + } +} diff --git a/src/plugins/expressions/public/services.ts b/src/plugins/expressions/public/services/index.ts similarity index 87% rename from src/plugins/expressions/public/services.ts rename to src/plugins/expressions/public/services/index.ts index a700e54d77e190..db473037a0a4a6 100644 --- a/src/plugins/expressions/public/services.ts +++ b/src/plugins/expressions/public/services/index.ts @@ -7,8 +7,8 @@ */ import { NotificationsStart } from 'kibana/public'; -import { createGetterSetter } from '../../kibana_utils/public'; -import { ExpressionsService, ExpressionRendererRegistry } from '../common'; +import { createGetterSetter } from '../../../kibana_utils/public'; +import { ExpressionsService, ExpressionRendererRegistry } from '../../common'; export const [getNotifications, setNotifications] = createGetterSetter( 'Notifications' @@ -23,3 +23,5 @@ export const [ getExpressionsService, setExpressionsService, ] = createGetterSetter('ExpressionsService'); + +export * from './expressions_services'; diff --git a/src/plugins/expressions/server/expression_functions/index.ts b/src/plugins/expressions/server/expression_functions/index.ts new file mode 100644 index 00000000000000..eb36291c613c67 --- /dev/null +++ b/src/plugins/expressions/server/expression_functions/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './ui_setting'; diff --git a/src/plugins/expressions/server/expression_functions/ui_setting.ts b/src/plugins/expressions/server/expression_functions/ui_setting.ts new file mode 100644 index 00000000000000..0d3d5b1c1f997b --- /dev/null +++ b/src/plugins/expressions/server/expression_functions/ui_setting.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { CoreSetup } from 'src/core/server'; +import { getUiSettingFn as getCommonUiSettingFn } from '../../common'; + +export function getUiSettingFn({ getStartServices }: Pick) { + return getCommonUiSettingFn({ + async getStartDependencies(getKibanaRequest) { + const [{ savedObjects, uiSettings }] = await getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(getKibanaRequest()); + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + + return { uiSettings: uiSettingsClient }; + }, + }); +} diff --git a/src/plugins/expressions/server/plugin.ts b/src/plugins/expressions/server/plugin.ts index 2c638d496ece62..2e45daf6e0f8c0 100644 --- a/src/plugins/expressions/server/plugin.ts +++ b/src/plugins/expressions/server/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { pick } from 'lodash'; import { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; import { ExpressionsService, ExpressionsServiceSetup, ExpressionsServiceStart } from '../common'; @@ -24,7 +25,7 @@ export class ExpressionsServerPlugin environment: 'server', }); - const setup = this.expressions.setup(); + const setup = this.expressions.setup(pick(core, 'getStartServices')); return Object.freeze(setup); } diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 12af0480fac93f..bc0980121b827e 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -347,7 +347,7 @@ export interface ExpressionFunctionDefinition>; name: Name; - type?: TypeToString>; + type?: TypeString | UnmappedTypeStrings; } // @public diff --git a/src/plugins/expressions/server/services/expressions_services.ts b/src/plugins/expressions/server/services/expressions_services.ts new file mode 100644 index 00000000000000..914745436f1518 --- /dev/null +++ b/src/plugins/expressions/server/services/expressions_services.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 { CoreSetup } from 'src/core/server'; +import { ExpressionsService as CommonExpressionsService } from '../../common'; +import { getUiSettingFn } from '../expression_functions'; + +export class ExpressionsService extends CommonExpressionsService { + setup({ getStartServices }: Pick) { + this.registerFunction(getUiSettingFn({ getStartServices })); + + return super.setup(); + } +} diff --git a/src/plugins/expressions/server/services/index.ts b/src/plugins/expressions/server/services/index.ts new file mode 100644 index 00000000000000..c239c4efdb02cc --- /dev/null +++ b/src/plugins/expressions/server/services/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './expressions_services'; diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index a12a2ff195211d..267769d33fba2c 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -280,7 +280,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[eCommerce] Top Selling Products', }), visState: - '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index 05a3d012d707c1..816322dbe5299c 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -242,7 +242,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Flights] Destination Weather', }), visState: - '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/index_pattern_management/public/constants.ts b/src/plugins/index_pattern_management/public/constants.ts new file mode 100644 index 00000000000000..e5010d133f0f30 --- /dev/null +++ b/src/plugins/index_pattern_management/public/constants.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 6c709fb14f08d7..7671a532d1cb86 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -19,14 +19,7 @@ import { } from './plugin'; import { IndexPatternManagmentContext } from './types'; -const createSetupContract = (): IndexPatternManagementSetup => ({ - creation: { - addCreationConfig: jest.fn(), - } as any, - list: { - addListConfig: jest.fn(), - } as any, -}); +const createSetupContract = (): IndexPatternManagementSetup => {}; const createStartContract = (): IndexPatternManagementStart => ({ creation: { diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index e3c156927bface..610b3541620b00 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -81,7 +81,10 @@ export class IndexPatternManagementPlugin }, }); - return this.indexPatternManagementService.setup({ httpClient: core.http }); + return this.indexPatternManagementService.setup({ + httpClient: core.http, + uiSettings: core.uiSettings, + }); } public start(core: CoreStart, plugins: IndexPatternManagementStartDependencies) { diff --git a/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts new file mode 100644 index 00000000000000..d1fc2fa242eb1b --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { RollupPrompt } from './rollup_prompt'; diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx similarity index 76% rename from x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js rename to src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx index 9306ab082dff49..81fcdaedb90c90 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js +++ b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; @@ -14,7 +15,7 @@ export const RollupPrompt = () => (

{i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text', + 'indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text', { defaultMessage: "Kibana's support for rollup index patterns is in beta. You might encounter issues using " + @@ -25,7 +26,7 @@ export const RollupPrompt = () => (

{i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text', + 'indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text', { defaultMessage: 'You can match a rollup index pattern against one rollup index and zero or more regular ' + diff --git a/src/plugins/index_pattern_management/public/service/creation/index.ts b/src/plugins/index_pattern_management/public/service/creation/index.ts index 51610bc83e371b..e1f464b01e5505 100644 --- a/src/plugins/index_pattern_management/public/service/creation/index.ts +++ b/src/plugins/index_pattern_management/public/service/creation/index.ts @@ -8,3 +8,5 @@ export { IndexPatternCreationConfig, IndexPatternCreationOption } from './config'; export { IndexPatternCreationManager } from './manager'; +// @ts-ignore +export { RollupIndexPatternCreationConfig } from './rollup_creation_config'; diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js similarity index 84% rename from x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js rename to src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js index 8e5203fca90347..2a85dfa01143c7 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js +++ b/src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js @@ -1,43 +1,44 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { i18n } from '@kbn/i18n'; import { RollupPrompt } from './components/rollup_prompt'; -import { IndexPatternCreationConfig } from '../../../../../src/plugins/index_pattern_management/public'; +import { IndexPatternCreationConfig } from '.'; const rollupIndexPatternTypeName = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName', + 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName', { defaultMessage: 'rollup index pattern' } ); const rollupIndexPatternButtonText = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonText', + 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText', { defaultMessage: 'Rollup index pattern' } ); const rollupIndexPatternButtonDescription = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonDescription', + 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription', { defaultMessage: 'Perform limited aggregations against summarized data' } ); const rollupIndexPatternNoMatchError = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.noMatchError', + 'indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError', { defaultMessage: 'Rollup index pattern error: must match one rollup index' } ); const rollupIndexPatternTooManyMatchesError = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.tooManyMatchesError', + 'indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError', { defaultMessage: 'Rollup index pattern error: can only match one rollup index' } ); const rollupIndexPatternIndexLabel = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.indexLabel', + 'indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel', { defaultMessage: 'Rollup' } ); @@ -127,7 +128,7 @@ export class RollupIndexPatternCreationConfig extends IndexPatternCreationConfig if (error) { const errorMessage = i18n.translate( - 'xpack.rollupJobs.editRollupIndexPattern.createIndex.uncaughtError', + 'indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError', { defaultMessage: 'Rollup index pattern error: {error}', values: { 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 f30ccfcb9f3ed7..19346dbf31d185 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 @@ -6,11 +6,22 @@ * Side Public License, v 1. */ -import { HttpSetup } from '../../../../core/public'; -import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; -import { IndexPatternListManager, IndexPatternListConfig } from './list'; +import { HttpSetup, CoreSetup } from '../../../../core/public'; +import { + IndexPatternCreationManager, + IndexPatternCreationConfig, + RollupIndexPatternCreationConfig, +} from './creation'; +import { + IndexPatternListManager, + IndexPatternListConfig, + RollupIndexPatternListConfig, +} from './list'; + +import { CONFIG_ROLLUPS } from '../constants'; interface SetupDependencies { httpClient: HttpSetup; + uiSettings: CoreSetup['uiSettings']; } /** @@ -27,17 +38,17 @@ export class IndexPatternManagementService { this.indexPatternListConfig = new IndexPatternListManager(); } - public setup({ httpClient }: SetupDependencies) { + public setup({ httpClient, uiSettings }: SetupDependencies) { const creationManagerSetup = this.indexPatternCreationManager.setup(httpClient); creationManagerSetup.addCreationConfig(IndexPatternCreationConfig); const indexPatternListConfigSetup = this.indexPatternListConfig.setup(); indexPatternListConfigSetup.addListConfig(IndexPatternListConfig); - return { - creation: creationManagerSetup, - list: indexPatternListConfigSetup, - }; + if (uiSettings.get(CONFIG_ROLLUPS)) { + creationManagerSetup.addCreationConfig(RollupIndexPatternCreationConfig); + indexPatternListConfigSetup.addListConfig(RollupIndexPatternListConfig); + } } public start() { diff --git a/src/plugins/index_pattern_management/public/service/list/index.ts b/src/plugins/index_pattern_management/public/service/list/index.ts index 620d4c7600733b..738b807ac76246 100644 --- a/src/plugins/index_pattern_management/public/service/list/index.ts +++ b/src/plugins/index_pattern_management/public/service/list/index.ts @@ -8,3 +8,5 @@ export { IndexPatternListConfig } from './config'; export { IndexPatternListManager } from './manager'; +// @ts-ignore +export { RollupIndexPatternListConfig } from './rollup_list_config'; diff --git a/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js b/src/plugins/index_pattern_management/public/service/list/rollup_list_config.js similarity index 86% rename from x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js rename to src/plugins/index_pattern_management/public/service/list/rollup_list_config.js index 43eee6ca27f9a0..9a80d5fd0d622b 100644 --- a/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js +++ b/src/plugins/index_pattern_management/public/service/list/rollup_list_config.js @@ -1,11 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { IndexPatternListConfig } from '../../../../../src/plugins/index_pattern_management/public'; +import { IndexPatternListConfig } from '.'; function isRollup(indexPattern) { return ( diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index a779ef540d72ec..d4fb5a708e4405 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -11,6 +11,7 @@ exports[`is rendered 1`] = ` onChange={[Function]} options={ Object { + "matchBrackets": "never", "minimap": Object { "enabled": false, }, @@ -39,6 +40,7 @@ exports[`is rendered 1`] = ` nodeType="div" onResize={[Function]} querySelector={null} + refreshMode="debounce" refreshRate={1000} skipOnMount={false} targetDomEl={null} diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 55e10e7861e518..251f05950da227 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -187,10 +187,16 @@ export class CodeEditor extends React.Component { wordBasedSuggestions: false, wordWrap: 'on', wrappingIndent: 'indent', + matchBrackets: 'never', ...options, }} /> - + ); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 1fa7d8e846c9da..e7c6b53ff97b3c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -148,6 +148,7 @@ export const applicationUsageSchema = { maps: commonSchema, ml: commonSchema, monitoring: commonSchema, + observabilityCases: commonSchema, 'observability-overview': commonSchema, osquery: commonSchema, security_account: commonSchema, diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts new file mode 100644 index 00000000000000..dda393a4203ecd --- /dev/null +++ b/src/plugins/management/common/locator.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { MANAGEMENT_APP_ID } from './contants'; +import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator'; + +test('locator has the right ID', () => { + const locator = new ManagementAppLocator(); + + expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR); +}); + +test('returns management app ID', async () => { + const locator = new ManagementAppLocator(); + const location = await locator.getLocation({ + sectionId: 'a', + appId: 'b', + }); + + expect(location).toMatchObject({ + app: MANAGEMENT_APP_ID, + }); +}); + +test('returns Kibana location for section ID and app ID pair', async () => { + const locator = new ManagementAppLocator(); + const location = await locator.getLocation({ + sectionId: 'ingest', + appId: 'index', + }); + + expect(location).toMatchObject({ + route: '/ingest/index', + state: {}, + }); +}); + +test('when app ID is not provided, returns path to just the section ID', async () => { + const locator = new ManagementAppLocator(); + const location = await locator.getLocation({ + sectionId: 'data', + }); + + expect(location).toMatchObject({ + route: '/data', + state: {}, + }); +}); diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts new file mode 100644 index 00000000000000..4a4a50f468adc6 --- /dev/null +++ b/src/plugins/management/common/locator.ts @@ -0,0 +1,32 @@ +/* + * 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 { SerializableState } from 'src/plugins/kibana_utils/common'; +import { LocatorDefinition } from 'src/plugins/share/common'; +import { MANAGEMENT_APP_ID } from './contants'; + +export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; + +export interface ManagementAppLocatorParams extends SerializableState { + sectionId: string; + appId?: string; +} + +export class ManagementAppLocator implements LocatorDefinition { + public readonly id = MANAGEMENT_APP_LOCATOR; + + public readonly getLocation = async (params: ManagementAppLocatorParams) => { + const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; + + return { + app: MANAGEMENT_APP_ID, + route, + state: {}, + }; + }; +} diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 6c8574f0242290..44c3f861709cee 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "share"], "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] } diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 4dcdd22d5d2095..70d853f32dfcc3 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -30,6 +30,14 @@ const createSetupContract = (): ManagementSetup => ({ stack: createManagementSectionMock(), } as unknown) as DefinedSections, }, + locator: { + getLocation: jest.fn(async () => ({ + app: 'MANAGEMENT', + route: '', + state: {}, + })), + navigate: jest.fn(), + }, }); const createStartContract = (): ManagementStart => ({ diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 1f96ec87171c5c..3289b2f6f5446a 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; +import type { SharePluginSetup, SharePluginStart } from 'src/plugins/share/public'; import { ManagementSetup, ManagementStart } from './types'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; import { @@ -24,6 +25,7 @@ import { } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; +import { ManagementAppLocator } from '../common/locator'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -32,9 +34,21 @@ import { ManagementSection } from './utils'; interface ManagementSetupDependencies { home?: HomePublicPluginSetup; + share: SharePluginSetup; } -export class ManagementPlugin implements Plugin { +interface ManagementStartDependencies { + share: SharePluginStart; +} + +export class ManagementPlugin + implements + Plugin< + ManagementSetup, + ManagementStart, + ManagementSetupDependencies, + ManagementStartDependencies + > { private readonly managementSections = new ManagementSectionsService(); private readonly appUpdater = new BehaviorSubject(() => { @@ -58,8 +72,9 @@ export class ManagementPlugin implements Plugin; } export interface DefinedSections { diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts index 5bb6a14e0b4504..349cab6206babc 100644 --- a/src/plugins/management/server/plugin.ts +++ b/src/plugins/management/server/plugin.ts @@ -7,21 +7,37 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { LocatorPublic } from 'src/plugins/share/common'; +import type { SharePluginSetup } from 'src/plugins/share/server'; +import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator'; import { capabilitiesProvider } from './capabilities_provider'; -export class ManagementServerPlugin implements Plugin { +interface ManagementSetupDependencies { + share: SharePluginSetup; +} + +export interface ManagementSetup { + locator: LocatorPublic; +} + +export class ManagementServerPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { share }: ManagementSetupDependencies) { this.logger.debug('management: Setup'); + const locator = share.url.locators.create(new ManagementAppLocator()); + core.capabilities.registerProvider(capabilitiesProvider); - return {}; + return { + locator, + }; } public start(core: CoreStart) { diff --git a/src/plugins/security_oss/kibana.json b/src/plugins/security_oss/kibana.json index 70e37d586f1db3..c93b5c3b60714d 100644 --- a/src/plugins/security_oss/kibana.json +++ b/src/plugins/security_oss/kibana.json @@ -1,5 +1,10 @@ { "id": "securityOss", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin exposes a limited set of security functionality to OSS plugins.", "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["security"], diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts new file mode 100644 index 00000000000000..8b5d8d45571942 --- /dev/null +++ b/src/plugins/share/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { LocatorDefinition, LocatorPublic } from './url_service'; diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 46fad0dee13b01..d13bb15f8c72ca 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -7,10 +7,12 @@ */ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; +export { LocatorDefinition } from '../common/url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; export { SharePluginSetup, SharePluginStart } from './plugin'; + export { ShareContext, ShareMenuProvider, diff --git a/src/plugins/share/server/index.ts b/src/plugins/share/server/index.ts index d1a0ed1f016f03..d820a362131a49 100644 --- a/src/plugins/share/server/index.ts +++ b/src/plugins/share/server/index.ts @@ -9,6 +9,8 @@ import { PluginInitializerContext } from '../../../core/server'; import { SharePlugin } from './plugin'; +export { SharePluginSetup, SharePluginStart } from './plugin'; + export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/spaces_oss/kibana.json b/src/plugins/spaces_oss/kibana.json index e048fb7ffb79c6..10127634618f1a 100644 --- a/src/plugins/spaces_oss/kibana.json +++ b/src/plugins/spaces_oss/kibana.json @@ -1,5 +1,10 @@ { "id": "spacesOss", + "owner": { + "name": "Platform Security", + "githubTeam": "kibana-security" + }, + "description": "This plugin exposes a limited set of spaces functionality to OSS plugins.", "version": "kibana", "server": false, "ui": true, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 7b6c4ba9788f14..51df1d3162b7c3 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -3970,6 +3970,137 @@ } } }, + "observabilityCases": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "observability-overview": { "properties": { "appId": { diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap index 17a91a4d43cc76..cbfece0b081c61 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap @@ -5,6 +5,7 @@ Object { "as": "tagloud_vis", "type": "render", "value": Object { + "syncColors": false, "visData": Object { "columns": Array [ Object { @@ -20,6 +21,12 @@ Object { "type": "datatable", }, "visParams": Object { + "bucket": Object { + "accessor": 1, + "format": Object { + "id": "number", + }, + }, "maxFontSize": 72, "metric": Object { "accessor": 0, @@ -29,6 +36,10 @@ Object { }, "minFontSize": 18, "orientation": "single", + "palette": Object { + "name": "default", + "type": "palette", + }, "scale": "linear", "showLabel": true, }, diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap index a8bc0b4c51678a..fed6fb54288f27 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap @@ -84,6 +84,9 @@ Object { "orientation": Array [ "single", ], + "palette": Array [ + "default", + ], "scale": Array [ "linear", ], diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap deleted file mode 100644 index 88ed7c66a79a2b..00000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap deleted file mode 100644 index d7707f64d8a4fc..00000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; - -exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js deleted file mode 100644 index 9e1d66b0a2faae..00000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js +++ /dev/null @@ -1,51 +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 React, { Component, Fragment } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiIconTip } from '@elastic/eui'; - -export class FeedbackMessage extends Component { - constructor() { - super(); - this.state = { shouldShowTruncate: false, shouldShowIncomplete: false }; - } - - render() { - if (!this.state.shouldShowTruncate && !this.state.shouldShowIncomplete) { - return ''; - } - - return ( - - {this.state.shouldShowTruncate && ( -

- -

- )} - {this.state.shouldShowIncomplete && ( -

- -

- )} - - } - /> - ); - } -} diff --git a/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx new file mode 100644 index 00000000000000..82663bbf7070ca --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy } from 'react'; +import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; +import { TagCloudVisParams, TagCloudTypeProps } from '../types'; + +const TagCloudOptionsLazy = lazy(() => import('./tag_cloud_options')); + +export const getTagCloudOptions = ({ palettes }: TagCloudTypeProps) => ( + props: VisEditorOptionsProps +) => ; diff --git a/src/plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js deleted file mode 100644 index 028a001cfbe634..00000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/label.js +++ /dev/null @@ -1,27 +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 React, { Component } from 'react'; - -export class Label extends Component { - constructor() { - super(); - this.state = { label: '', shouldShowLabel: true }; - } - - render() { - return ( -
- {this.state.label} -
- ); - } -} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js deleted file mode 100644 index 254d210eebf376..00000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js +++ /dev/null @@ -1,409 +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 d3 from 'd3'; -import d3TagCloud from 'd3-cloud'; -import { EventEmitter } from 'events'; - -const ORIENTATIONS = { - single: () => 0, - 'right angled': (tag) => { - return hashWithinRange(tag.text, 2) * 90; - }, - multiple: (tag) => { - return hashWithinRange(tag.text, 12) * 15 - 90; //fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset) - }, -}; -const D3_SCALING_FUNCTIONS = { - linear: () => d3.scale.linear(), - log: () => d3.scale.log(), - 'square root': () => d3.scale.sqrt(), -}; - -export class TagCloud extends EventEmitter { - constructor(domNode, colorScale) { - super(); - - //DOM - this._element = domNode; - this._d3SvgContainer = d3.select(this._element).append('svg'); - this._svgGroup = this._d3SvgContainer.append('g'); - this._size = [1, 1]; - this.resize(); - - //SETTING (non-configurable) - /** - * the fontFamily should be set explicitly for calculating a layout - * and to avoid words overlapping - */ - this._fontFamily = 'Inter UI, sans-serif'; - this._fontStyle = 'normal'; - this._fontWeight = 'normal'; - this._spiral = 'archimedean'; //layout shape - this._timeInterval = 1000; //time allowed for layout algorithm - this._padding = 5; - - //OPTIONS - this._orientation = 'single'; - this._minFontSize = 10; - this._maxFontSize = 36; - this._textScale = 'linear'; - this._optionsAsString = null; - - //DATA - this._words = null; - - //UTIL - this._colorScale = colorScale; - this._setTimeoutId = null; - this._pendingJob = null; - this._layoutIsUpdating = null; - this._allInViewBox = false; - this._DOMisUpdating = false; - } - - setOptions(options) { - if (JSON.stringify(options) === this._optionsAsString) { - return; - } - this._optionsAsString = JSON.stringify(options); - this._orientation = options.orientation; - this._minFontSize = Math.min(options.minFontSize, options.maxFontSize); - this._maxFontSize = Math.max(options.minFontSize, options.maxFontSize); - this._textScale = options.scale; - this._invalidate(false); - } - - resize() { - const newWidth = this._element.offsetWidth; - const newHeight = this._element.offsetHeight; - - if (newWidth === this._size[0] && newHeight === this._size[1]) { - return; - } - - const wasInside = this._size[0] >= this._cloudWidth && this._size[1] >= this._cloudHeight; - const willBeInside = this._cloudWidth <= newWidth && this._cloudHeight <= newHeight; - this._size[0] = newWidth; - this._size[1] = newHeight; - if (wasInside && willBeInside && this._allInViewBox) { - this._invalidate(true); - } else { - this._invalidate(false); - } - } - - setData(data) { - this._words = data; - this._invalidate(false); - } - - destroy() { - clearTimeout(this._setTimeoutId); - this._element.innerHTML = ''; - } - - getStatus() { - return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE; - } - - _updateContainerSize() { - this._d3SvgContainer.attr('width', this._size[0]); - this._d3SvgContainer.attr('height', this._size[1]); - this._svgGroup.attr('width', this._size[0]); - this._svgGroup.attr('height', this._size[1]); - } - - _isJobRunning() { - return this._setTimeoutId || this._layoutIsUpdating || this._DOMisUpdating; - } - - async _processPendingJob() { - if (!this._pendingJob) { - return; - } - - if (this._isJobRunning()) { - return; - } - - this._completedJob = null; - const job = await this._pickPendingJob(); - if (job.words.length) { - if (job.refreshLayout) { - await this._updateLayout(job); - } - await this._updateDOM(job); - const cloudBBox = this._svgGroup[0][0].getBBox(); - this._cloudWidth = cloudBBox.width; - this._cloudHeight = cloudBBox.height; - this._allInViewBox = - cloudBBox.x >= 0 && - cloudBBox.y >= 0 && - cloudBBox.x + cloudBBox.width <= this._element.offsetWidth && - cloudBBox.y + cloudBBox.height <= this._element.offsetHeight; - } else { - this._emptyDOM(job); - } - - if (this._pendingJob) { - this._processPendingJob(); //pick up next job - } else { - this._completedJob = job; - this.emit('renderComplete'); - } - } - - async _pickPendingJob() { - return await new Promise((resolve) => { - this._setTimeoutId = setTimeout(async () => { - const job = this._pendingJob; - this._pendingJob = null; - this._setTimeoutId = null; - resolve(job); - }, 0); - }); - } - - _emptyDOM() { - this._svgGroup.selectAll('text').remove(); - this._cloudWidth = 0; - this._cloudHeight = 0; - this._allInViewBox = true; - this._DOMisUpdating = false; - } - - async _updateDOM(job) { - const canSkipDomUpdate = this._pendingJob || this._setTimeoutId; - if (canSkipDomUpdate) { - this._DOMisUpdating = false; - return; - } - - this._DOMisUpdating = true; - const affineTransform = positionWord.bind( - null, - this._element.offsetWidth / 2, - this._element.offsetHeight / 2 - ); - const svgTextNodes = this._svgGroup.selectAll('text'); - const stage = svgTextNodes.data(job.words, getText); - - await new Promise((resolve) => { - const enterSelection = stage.enter(); - const enteringTags = enterSelection.append('text'); - enteringTags.style('font-size', getSizeInPixels); - enteringTags.style('font-style', this._fontStyle); - enteringTags.style('font-weight', () => this._fontWeight); - enteringTags.style('font-family', () => this._fontFamily); - enteringTags.style('fill', this.getFill.bind(this)); - enteringTags.attr('text-anchor', () => 'middle'); - enteringTags.attr('transform', affineTransform); - enteringTags.attr('data-test-subj', getDisplayText); - enteringTags.text(getDisplayText); - - const self = this; - enteringTags.on({ - click: function (event) { - self.emit('select', event); - }, - mouseover: function () { - d3.select(this).style('cursor', 'pointer'); - }, - mouseout: function () { - d3.select(this).style('cursor', 'default'); - }, - }); - - const movingTags = stage.transition(); - movingTags.duration(600); - movingTags.style('font-size', getSizeInPixels); - movingTags.style('font-style', this._fontStyle); - movingTags.style('font-weight', () => this._fontWeight); - movingTags.style('font-family', () => this._fontFamily); - movingTags.attr('transform', affineTransform); - - const exitingTags = stage.exit(); - const exitTransition = exitingTags.transition(); - exitTransition.duration(200); - exitingTags.style('fill-opacity', 1e-6); - exitingTags.attr('font-size', 1); - exitingTags.remove(); - - let exits = 0; - let moves = 0; - const resolveWhenDone = () => { - if (exits === 0 && moves === 0) { - this._DOMisUpdating = false; - resolve(true); - } - }; - exitTransition.each(() => exits++); - exitTransition.each('end', () => { - exits--; - resolveWhenDone(); - }); - movingTags.each(() => moves++); - movingTags.each('end', () => { - moves--; - resolveWhenDone(); - }); - }); - } - - _makeTextSizeMapper() { - const mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale](); - const range = - this._words.length === 1 - ? [this._maxFontSize, this._maxFontSize] - : [this._minFontSize, this._maxFontSize]; - mapSizeToFontSize.range(range); - if (this._words) { - mapSizeToFontSize.domain(d3.extent(this._words, getValue)); - } - return mapSizeToFontSize; - } - - _makeNewJob() { - return { - refreshLayout: true, - size: this._size.slice(), - words: this._words, - }; - } - - _makeJobPreservingLayout() { - return { - refreshLayout: false, - size: this._size.slice(), - words: this._completedJob.words.map((tag) => { - return { - x: tag.x, - y: tag.y, - rotate: tag.rotate, - size: tag.size, - rawText: tag.rawText || tag.text, - displayText: tag.displayText, - meta: tag.meta, - }; - }), - }; - } - - _invalidate(keepLayout) { - if (!this._words) { - return; - } - - this._updateContainerSize(); - - const canReuseLayout = keepLayout && !this._isJobRunning() && this._completedJob; - this._pendingJob = canReuseLayout ? this._makeJobPreservingLayout() : this._makeNewJob(); - this._processPendingJob(); - } - - async _updateLayout(job) { - if (job.size[0] <= 0 || job.size[1] <= 0) { - // If either width or height isn't above 0 we don't relayout anything, - // since the d3-cloud will be stuck in an infinite loop otherwise. - return; - } - - const mapSizeToFontSize = this._makeTextSizeMapper(); - const tagCloudLayoutGenerator = d3TagCloud(); - tagCloudLayoutGenerator.size(job.size); - tagCloudLayoutGenerator.padding(this._padding); - tagCloudLayoutGenerator.rotate(ORIENTATIONS[this._orientation]); - tagCloudLayoutGenerator.font(this._fontFamily); - tagCloudLayoutGenerator.fontStyle(this._fontStyle); - tagCloudLayoutGenerator.fontWeight(this._fontWeight); - tagCloudLayoutGenerator.fontSize((tag) => mapSizeToFontSize(tag.value)); - tagCloudLayoutGenerator.random(seed); - tagCloudLayoutGenerator.spiral(this._spiral); - tagCloudLayoutGenerator.words(job.words); - tagCloudLayoutGenerator.text(getDisplayText); - tagCloudLayoutGenerator.timeInterval(this._timeInterval); - - this._layoutIsUpdating = true; - await new Promise((resolve) => { - tagCloudLayoutGenerator.on('end', () => { - this._layoutIsUpdating = false; - resolve(true); - }); - tagCloudLayoutGenerator.start(); - }); - } - - /** - * Returns debug info. For debugging only. - * @return {*} - */ - getDebugInfo() { - const debug = {}; - debug.positions = this._completedJob - ? this._completedJob.words.map((tag) => { - return { - displayText: tag.displayText, - rawText: tag.rawText || tag.text, - x: tag.x, - y: tag.y, - rotate: tag.rotate, - }; - }) - : []; - debug.size = { - width: this._size[0], - height: this._size[1], - }; - return debug; - } - - getFill(tag) { - return this._colorScale(tag.text); - } -} - -TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 }; - -function seed() { - return 0.5; //constant seed (not random) to ensure constant layouts for identical data -} - -function getText(word) { - return word.rawText; -} - -function getDisplayText(word) { - return word.displayText; -} - -function positionWord(xTranslate, yTranslate, word) { - if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) { - //move off-screen - return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`; - } - - return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`; -} - -function getValue(tag) { - return tag.value; -} - -function getSizeInPixels(tag) { - return `${tag.size}px`; -} - -function hashWithinRange(str, max) { - str = JSON.stringify(str); - let hash = 0; - for (const ch of str) { - hash = (hash * 31 + ch.charCodeAt(0)) % max; - } - return Math.abs(hash) % max; -} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss index 37867f1ed1c178..51b5e9dedd8442 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss @@ -5,18 +5,14 @@ // tgcChart__legend--small // tgcChart__legend-isLoading -.tgcChart__container, .tgcChart__wrapper { +.tgcChart__wrapper { flex: 1 1 0; display: flex; + flex-direction: column; } -.tgcChart { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; +.tgcChart__wrapper text { + cursor: pointer; } .tgcChart__label { @@ -24,3 +20,7 @@ text-align: center; font-weight: $euiFontWeightBold; } + +.tgcChart__warning { + width: $euiSize; +} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js deleted file mode 100644 index 2fb2be0ace7cdc..00000000000000 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js +++ /dev/null @@ -1,506 +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 _ from 'lodash'; -import d3 from 'd3'; -import 'jest-canvas-mock'; - -import { fromNode, delay } from 'bluebird'; -import { TagCloud } from './tag_cloud'; -import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest'; - -describe('tag cloud tests', () => { - let SVGElementGetBBoxSpyInstance; - let HTMLElementOffsetMockInstance; - - beforeEach(() => { - setupDOM(); - }); - - afterEach(() => { - SVGElementGetBBoxSpyInstance.mockRestore(); - HTMLElementOffsetMockInstance.mockRestore(); - }); - - const minValue = 1; - const maxValue = 9; - const midValue = (minValue + maxValue) / 2; - const baseTest = { - data: [ - { rawText: 'foo', displayText: 'foo', value: minValue }, - { rawText: 'bar', displayText: 'bar', value: midValue }, - { rawText: 'foobar', displayText: 'foobar', value: maxValue }, - ], - options: { - orientation: 'single', - scale: 'linear', - minFontSize: 10, - maxFontSize: 36, - }, - expected: [ - { - text: 'foo', - fontSize: '10px', - }, - { - text: 'bar', - fontSize: '23px', - }, - { - text: 'foobar', - fontSize: '36px', - }, - ], - }; - - const singleLayoutTest = _.cloneDeep(baseTest); - - const rightAngleLayoutTest = _.cloneDeep(baseTest); - rightAngleLayoutTest.options.orientation = 'right angled'; - - const multiLayoutTest = _.cloneDeep(baseTest); - multiLayoutTest.options.orientation = 'multiple'; - - const mapWithLog = d3.scale.log(); - mapWithLog.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]); - mapWithLog.domain([minValue, maxValue]); - const logScaleTest = _.cloneDeep(baseTest); - logScaleTest.options.scale = 'log'; - logScaleTest.expected[1].fontSize = Math.round(mapWithLog(midValue)) + 'px'; - - const mapWithSqrt = d3.scale.sqrt(); - mapWithSqrt.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]); - mapWithSqrt.domain([minValue, maxValue]); - const sqrtScaleTest = _.cloneDeep(baseTest); - sqrtScaleTest.options.scale = 'square root'; - sqrtScaleTest.expected[1].fontSize = Math.round(mapWithSqrt(midValue)) + 'px'; - - const biggerFontTest = _.cloneDeep(baseTest); - biggerFontTest.options.minFontSize = 36; - biggerFontTest.options.maxFontSize = 72; - biggerFontTest.expected[0].fontSize = '36px'; - biggerFontTest.expected[1].fontSize = '54px'; - biggerFontTest.expected[2].fontSize = '72px'; - - const trimDataTest = _.cloneDeep(baseTest); - trimDataTest.data.splice(1, 1); - trimDataTest.expected.splice(1, 1); - - let domNode; - let tagCloud; - - const colorScale = d3.scale - .ordinal() - .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']); - - function setupDOM() { - domNode = document.createElement('div'); - SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(); - HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512); - - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } - - [ - singleLayoutTest, - rightAngleLayoutTest, - multiLayoutTest, - logScaleTest, - sqrtScaleTest, - biggerFontTest, - trimDataTest, - ].forEach(function (currentTest) { - describe(`should position elements correctly for options: ${JSON.stringify( - currentTest.options - )}`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(currentTest.data); - tagCloud.setOptions(currentTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(currentTest.expected, textElements, tagCloud); - }) - ); - }); - }); - - [5, 100, 200, 300, 500].forEach((timeout) => { - describe(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { - beforeEach(async () => { - //TagCloud takes at least 600ms to complete (due to d3 animation) - //renderComplete should only notify at the last one - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - //this timeout modifies the settings before the cloud is rendered. - //the cloud needs to use the correct options - setTimeout(() => tagCloud.setOptions(logScaleTest.options), timeout); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - }); - - describe('should use the latest state before notifying (when modifying options multiple times)', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - tagCloud.setOptions(logScaleTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should use the latest state before notifying (when modifying data multiple times)', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - tagCloud.setData(trimDataTest.data); - - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(trimDataTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should not get multiple render-events', () => { - let counter; - beforeEach(() => { - counter = 0; - - return new Promise((resolve, reject) => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - setTimeout(() => { - //this should be overridden by later changes - tagCloud.setData(sqrtScaleTest.data); - tagCloud.setOptions(sqrtScaleTest.options); - }, 100); - - setTimeout(() => { - //latest change - tagCloud.setData(logScaleTest.data); - tagCloud.setOptions(logScaleTest.options); - }, 300); - - tagCloud.on('renderComplete', function onRender() { - if (counter > 0) { - reject('Should not get multiple render events'); - } - counter += 1; - resolve(true); - }); - }); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(logScaleTest.expected, textElements, tagCloud); - }) - ); - }); - - describe('should show correct data when state-updates are interleaved with resize event', () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(logScaleTest.data); - tagCloud.setOptions(logScaleTest.options); - - await delay(1000); //let layout run - - SVGElementGetBBoxSpyInstance.mockRestore(); - SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600); - - tagCloud.resize(); //triggers new layout - setTimeout(() => { - //change the options at the very end too - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - }, 200); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - test( - 'positions should be ok', - handleExpectedBlip(() => { - const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(baseTest.expected, textElements, tagCloud); - }) - ); - }); - - describe(`should not put elements in view when container is too small`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test('completeness should not be ok', () => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); - }); - test('positions should not be ok', () => { - const textElements = domNode.querySelectorAll('text'); - for (let i = 0; i < textElements; i++) { - const bbox = textElements[i].getBoundingClientRect(); - verifyBbox(bbox, false, tagCloud); - } - }); - }); - - describe(`tags should fit after making container bigger`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - //make bigger - tagCloud._size = [600, 600]; - tagCloud.resize(); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test( - 'completeness should be ok', - handleExpectedBlip(() => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); - }) - ); - }); - - describe(`tags should no longer fit after making container smaller`, () => { - beforeEach(async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - //make smaller - tagCloud._size = []; - tagCloud.resize(); - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - }); - - afterEach(teardownDOM); - - test('completeness should not be ok', () => { - expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); - }); - }); - - describe('tagcloudscreenshot', () => { - afterEach(teardownDOM); - - test('should render simple image', async () => { - tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(baseTest.data); - tagCloud.setOptions(baseTest.options); - - await fromNode((cb) => tagCloud.once('renderComplete', cb)); - - expect(domNode.innerHTML).toMatchSnapshot(); - }); - }); - - function verifyTagProperties(expectedValues, actualElements, tagCloud) { - expect(actualElements.length).toEqual(expectedValues.length); - expectedValues.forEach((test, index) => { - try { - expect(actualElements[index].style.fontSize).toEqual(test.fontSize); - } catch (e) { - throw new Error('fontsize is not correct: ' + e.message); - } - try { - expect(actualElements[index].innerHTML).toEqual(test.text); - } catch (e) { - throw new Error('fontsize is not correct: ' + e.message); - } - isInsideContainer(actualElements[index], tagCloud); - }); - } - - function isInsideContainer(actualElement, tagCloud) { - const bbox = actualElement.getBoundingClientRect(); - verifyBbox(bbox, true, tagCloud); - } - - function verifyBbox(bbox, shouldBeInside, tagCloud) { - const message = ` | bbox-of-tag: ${JSON.stringify([ - bbox.left, - bbox.top, - bbox.right, - bbox.bottom, - ])} vs - bbox-of-container: ${domNode.offsetWidth},${domNode.offsetHeight} - debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`; - - try { - expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message - ); - } - try { - expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'bottom boundary of tag should have been ' + - (shouldBeInside ? 'inside' : 'outside') + - message - ); - } - try { - expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message - ); - } - try { - expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside); - } catch (e) { - throw new Error( - 'right boundary of tag should have been ' + - (shouldBeInside ? 'inside' : 'outside') + - message - ); - } - } - - /** - * In CI, this entire suite "blips" about 1/5 times. - * This blip causes the majority of these tests fail for the exact same reason: One tag is centered inside the container, - * while the others are moved out. - * This has not been reproduced locally yet. - * It may be an issue with the 3rd party d3-cloud that snags. - * - * The test suite should continue to catch reliably catch regressions of other sorts: unexpected and other uncaught errors, - * scaling issues, ordering issues - * - */ - function shouldAssert() { - const debugInfo = tagCloud.getDebugInfo(); - const count = debugInfo.positions.length; - const largest = debugInfo.positions.pop(); //test suite puts largest tag at the end. - - const centered = largest[1] === 0 && largest[2] === 0; - const halfWidth = debugInfo.size.width / 2; - const halfHeight = debugInfo.size.height / 2; - const inside = debugInfo.positions.filter((position) => { - const x = position.x + halfWidth; - const y = position.y + halfHeight; - return 0 <= x && x <= debugInfo.size.width && 0 <= y && y <= debugInfo.size.height; - }); - - return centered && inside.length === count - 1; - } - - function handleExpectedBlip(assertion) { - return () => { - if (!shouldAssert()) { - return; - } - assertion(); - }; - } -}); diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx new file mode 100644 index 00000000000000..b4d4e70d5ffe3e --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { Wordcloud, Settings } from '@elastic/charts'; +import { chartPluginMock } from '../../../charts/public/mocks'; +import type { Datatable } from '../../../expressions/public'; +import { mount } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import TagCloudChart, { TagCloudChartProps } from './tag_cloud_chart'; +import { TagCloudVisParams } from '../types'; + +jest.mock('../services', () => ({ + getFormatService: jest.fn(() => { + return { + deserialize: jest.fn(), + }; + }), +})); + +const palettesRegistry = chartPluginMock.createPaletteRegistry(); +const visData = ({ + columns: [ + { + id: 'col-0', + name: 'geo.dest: Descending', + }, + { + id: 'col-1', + name: 'Count', + }, + ], + rows: [ + { 'col-0': 'CN', 'col-1': 26 }, + { 'col-0': 'IN', 'col-1': 17 }, + { 'col-0': 'US', 'col-1': 6 }, + { 'col-0': 'DE', 'col-1': 4 }, + { 'col-0': 'BR', 'col-1': 3 }, + ], +} as unknown) as Datatable; + +const visParams = { + bucket: { accessor: 0, format: {} }, + metric: { accessor: 1, format: {} }, + scale: 'linear', + orientation: 'single', + palette: { + type: 'palette', + name: 'default', + }, + minFontSize: 12, + maxFontSize: 70, + showLabel: true, +} as TagCloudVisParams; + +describe('TagCloudChart', function () { + let wrapperProps: TagCloudChartProps; + + beforeAll(() => { + wrapperProps = { + visData, + visParams, + palettesRegistry, + fireEvent: jest.fn(), + renderComplete: jest.fn(), + syncColors: false, + visType: 'tagcloud', + }; + }); + + it('renders the Wordcloud component', async () => { + const component = mount(); + expect(component.find(Wordcloud).length).toBe(1); + }); + + it('renders the label correctly', async () => { + const component = mount(); + const label = findTestSubject(component, 'tagCloudLabel'); + expect(label.text()).toEqual('geo.dest: Descending - Count'); + }); + + it('not renders the label if showLabel setting is off', async () => { + const newVisParams = { ...visParams, showLabel: false }; + const newProps = { ...wrapperProps, visParams: newVisParams }; + const component = mount(); + const label = findTestSubject(component, 'tagCloudLabel'); + expect(label.length).toBe(0); + }); + + it('receives the data on the correct format', () => { + const component = mount(); + expect(component.find(Wordcloud).prop('data')).toStrictEqual([ + { + color: 'black', + text: 'CN', + weight: 1, + }, + { + color: 'black', + text: 'IN', + weight: 0.6086956521739131, + }, + { + color: 'black', + text: 'US', + weight: 0.13043478260869565, + }, + { + color: 'black', + text: 'DE', + weight: 0.043478260869565216, + }, + { + color: 'black', + text: 'BR', + weight: 0, + }, + ]); + }); + + it('sets the angles correctly', async () => { + const newVisParams = { ...visParams, orientation: 'right angled' } as TagCloudVisParams; + const newProps = { ...wrapperProps, visParams: newVisParams }; + const component = mount(); + expect(component.find(Wordcloud).prop('endAngle')).toBe(90); + expect(component.find(Wordcloud).prop('angleCount')).toBe(2); + }); + + it('calls filter callback', () => { + const component = mount(); + component.find(Settings).prop('onElementClick')!([ + [ + { + text: 'BR', + weight: 0.17391304347826086, + color: '#d36086', + }, + { + specId: 'tagCloud', + key: 'tagCloud', + }, + ], + ]); + expect(wrapperProps.fireEvent).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx index f668e22815b60f..b89fe2fa90ede0 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx @@ -6,64 +6,225 @@ * Side Public License, v 1. */ -import React, { useEffect, useMemo, useRef } from 'react'; -import { EuiResizeObserver } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { throttle } from 'lodash'; - -import { TagCloudVisDependencies } from '../plugin'; +import { EuiIconTip, EuiResizeObserver } from '@elastic/eui'; +import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts'; +import type { PaletteRegistry } from '../../../charts/public'; +import type { IInterpreterRenderHandlers } from '../../../expressions/public'; +import { getFormatService } from '../services'; import { TagCloudVisRenderValue } from '../tag_cloud_fn'; -// @ts-ignore -import { TagCloudVisualization } from './tag_cloud_visualization'; import './tag_cloud.scss'; -type TagCloudChartProps = TagCloudVisDependencies & - TagCloudVisRenderValue & { - fireEvent: (event: any) => void; - renderComplete: () => void; - }; +const MAX_TAG_COUNT = 200; + +export type TagCloudChartProps = TagCloudVisRenderValue & { + fireEvent: IInterpreterRenderHandlers['event']; + renderComplete: IInterpreterRenderHandlers['done']; + palettesRegistry: PaletteRegistry; +}; + +const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) => + ((value - x1) * (y2 - x2)) / (y1 - x1) + x2; + +const getColor = ( + palettes: PaletteRegistry, + activePalette: string, + text: string, + values: string[], + syncColors: boolean +) => { + return palettes?.get(activePalette).getCategoricalColor( + [ + { + name: text, + rankAtDepth: values.length ? values.findIndex((name) => name === text) : 0, + totalSeriesAtDepth: values.length || 1, + }, + ], + { + maxDepth: 1, + totalSeries: values.length || 1, + behindText: false, + syncColors, + } + ); +}; + +const ORIENTATIONS = { + single: { + endAngle: 0, + angleCount: 360, + }, + 'right angled': { + endAngle: 90, + angleCount: 2, + }, + multiple: { + endAngle: -90, + angleCount: 12, + }, +}; export const TagCloudChart = ({ - colors, visData, visParams, + palettesRegistry, fireEvent, renderComplete, + syncColors, }: TagCloudChartProps) => { - const chartDiv = useRef(null); - const visController = useRef(null); + const [warning, setWarning] = useState(false); + const { bucket, metric, scale, palette, showLabel, orientation } = visParams; + const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null; - useEffect(() => { - if (chartDiv.current) { - visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent); - } - return () => { - visController.current.destroy(); - visController.current = null; - }; - }, [colors, fireEvent]); - - useEffect(() => { - if (visController.current) { - visController.current.render(visData, visParams).then(renderComplete); - } - }, [visData, visParams, renderComplete]); + const tagCloudData = useMemo(() => { + const tagColumn = bucket ? visData.columns[bucket.accessor].id : -1; + const metricColumn = visData.columns[metric.accessor]?.id; + + const metrics = visData.rows.map((row) => row[metricColumn]); + const values = bucket ? visData.rows.map((row) => row[tagColumn]) : []; + const maxValue = Math.max(...metrics); + const minValue = Math.min(...metrics); + + return visData.rows.map((row) => { + const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn]; + return { + text: (bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag) as string, + weight: + tag === 'all' || visData.rows.length <= 1 + ? 1 + : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0, + color: getColor(palettesRegistry, palette.name, tag, values, syncColors) || 'rgba(0,0,0,0)', + }; + }); + }, [ + bucket, + bucketFormatter, + metric.accessor, + palette.name, + palettesRegistry, + syncColors, + visData.columns, + visData.rows, + ]); + + const label = bucket + ? `${visData.columns[bucket.accessor].name} - ${visData.columns[metric.accessor].name}` + : ''; + + const onRenderChange = useCallback( + (isRendered) => { + if (isRendered) { + renderComplete(); + } + }, + [renderComplete] + ); - const updateChartSize = useMemo( + const updateChart = useMemo( () => throttle(() => { - if (visController.current) { - visController.current.render(visData, visParams).then(renderComplete); - } + setWarning(false); }, 300), - [renderComplete, visData, visParams] + [] + ); + + const handleWordClick = useCallback( + (d) => { + if (!bucket) { + return; + } + const termsBucket = visData.columns[bucket.accessor]; + const clickedValue = d[0][0].text; + + const rowIndex = visData.rows.findIndex((row) => { + const formattedValue = bucketFormatter + ? bucketFormatter.convert(row[termsBucket.id], 'text') + : row[termsBucket.id]; + return formattedValue === clickedValue; + }); + + if (rowIndex < 0) { + return; + } + + fireEvent({ + name: 'filterBucket', + data: { + data: [ + { + table: visData, + column: bucket.accessor, + row: rowIndex, + }, + ], + }, + }); + }, + [bucket, bucketFormatter, fireEvent, visData] ); return ( - + {(resizeRef) => ( -
-
+
+ + + { + setWarning(true); + }} + /> + + {label && showLabel && ( +
+ {label} +
+ )} + {warning && ( +
+ + } + /> +
+ )} + {tagCloudData.length > MAX_TAG_COUNT && ( +
+ + } + /> +
+ )}
)} diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index d5e005a6386806..6682799a8038ad 100644 --- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -6,16 +6,22 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; -import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; +import type { PaletteRegistry } from '../../../charts/public'; +import { VisEditorOptionsProps } from '../../../visualizations/public'; +import { SelectOption, SwitchOption, PalettePicker } from '../../../vis_default_editor/public'; import { ValidatedDualRange } from '../../../kibana_react/public'; -import { TagCloudVisParams } from '../types'; +import { TagCloudVisParams, TagCloudTypeProps } from '../types'; import { collections } from './collections'; -function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps) { +interface TagCloudOptionsProps + extends VisEditorOptionsProps, + TagCloudTypeProps {} + +function TagCloudOptions({ stateParams, setValue, palettes }: TagCloudOptionsProps) { + const [palettesRegistry, setPalettesRegistry] = useState(undefined); const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { setValue('minFontSize', Number(minFontSize)); setValue('maxFontSize', Number(maxFontSize)); @@ -24,6 +30,14 @@ function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps { + const fetchPalettes = async () => { + const palettesService = await palettes?.getPalettes(); + setPalettesRegistry(palettesService); + }; + fetchPalettes(); + }, [palettes]); + return ( + {palettesRegistry && ( + { + setValue(paramName, value); + }} + /> + )} + { - if (!this._visParams.bucket) { - return; - } - - fireEvent({ - name: 'filterBucket', - data: { - data: [ - { - table: event.meta.data, - column: 0, - row: event.meta.rowIndex, - }, - ], - }, - }); - }); - this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete'); - - this._feedbackNode = document.createElement('div'); - this._containerNode.appendChild(this._feedbackNode); - this._feedbackMessage = React.createRef(); - render( - - - , - this._feedbackNode - ); - - this._labelNode = document.createElement('div'); - this._containerNode.appendChild(this._labelNode); - this._label = React.createRef(); - render(