diff --git a/docs/developer/getting-started/sample-data.asciidoc b/docs/developer/getting-started/sample-data.asciidoc index 0d313cbabe64e4..2454c9d8a61468 100644 --- a/docs/developer/getting-started/sample-data.asciidoc +++ b/docs/developer/getting-started/sample-data.asciidoc @@ -6,7 +6,7 @@ There are a couple ways to easily get data ingested into {es}. [discrete] === Sample data packages available for one click installation -The easiest is to install one or more of our vailable sample data packages. If you have no data, you should be +The easiest is to install one or more of our available sample data packages. If you have no data, you should be prompted to install when running {kib} for the first time. You can also access and install the sample data packages by going to the home page and clicking "add sample data". @@ -27,5 +27,5 @@ Make sure to execute `node scripts/makelogs` *after* {es} is up and running! [discrete] === CSV upload -If running with a platinum or trial license, you can also use the CSV uploader provided inside the Machine learning app. -Navigate to the Data visualizer to upload your data from a file. \ No newline at end of file +You can also use the CSV uploader provided on the {kib} home page. +Navigate to **Add data** > **Upload file** to upload your data from a file. \ No newline at end of file diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 8c0aa12ffc4c61..5e18d934863aa9 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -46,8 +46,7 @@ image::images/add-data-fleet.png[Add data using Fleet] [[upload-data-kibana]] === Upload a file -experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the File -Data Visualizer. You can upload a file up to 100 MB. This value is configurable up to 1 GB in +experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the {file-data-viz}. You can upload a file up to 100 MB. This value is configurable up to 1 GB in <>. To upload a file with geospatial data, refer to <>. diff --git a/docs/setup/images/add-data-fv.png b/docs/setup/images/add-data-fv.png old mode 100755 new mode 100644 index 45313d133822c1..7e253cdd0229d7 Binary files a/docs/setup/images/add-data-fv.png and b/docs/setup/images/add-data-fv.png differ diff --git a/docs/setup/images/add-data-tutorials.png b/docs/setup/images/add-data-tutorials.png index 74deedc57b42ed..782b44e383772c 100644 Binary files a/docs/setup/images/add-data-tutorials.png and b/docs/setup/images/add-data-tutorials.png differ diff --git a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc index f64c120f61298f..d6b90a4f19e112 100644 --- a/docs/user/production-considerations/task-manager-health-monitoring.asciidoc +++ b/docs/user/production-considerations/task-manager-health-monitoring.asciidoc @@ -6,6 +6,8 @@ Health monitoring ++++ +experimental[] + The Task Manager has an internal monitoring mechanism to keep track of a variety of metrics, which can be consumed with either the health monitoring API or the {kib} server log. The health monitoring API provides a reliable endpoint that can be monitored. diff --git a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc index 5e75aef0d9570f..c6a7b7f3d53fdc 100644 --- a/docs/user/production-considerations/task-manager-troubleshooting.asciidoc +++ b/docs/user/production-considerations/task-manager-troubleshooting.asciidoc @@ -60,6 +60,8 @@ For details on scaling Task Manager, see <>. [[task-manager-diagnosing-root-cause]] ==== Diagnose a root cause for drift +experimental[] + The following guide helps you identify a root cause for _drift_ by making sense of the output from the <> endpoint. By analyzing the different sections of the output, you can evaluate different theories that explain the drift in a deployment. diff --git a/examples/index_pattern_field_editor_example/README.md b/examples/index_pattern_field_editor_example/README.md new file mode 100644 index 00000000000000..35ae814fc10e25 --- /dev/null +++ b/examples/index_pattern_field_editor_example/README.md @@ -0,0 +1,7 @@ +## index pattern field editor example + +This example index pattern field editor app shows how to: + - Edit index pattern fields via flyout + - Delete index pattern runtime fields with modal confirm prompt + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/index_pattern_field_editor_example/kibana.json b/examples/index_pattern_field_editor_example/kibana.json new file mode 100644 index 00000000000000..c522e6698ac3d6 --- /dev/null +++ b/examples/index_pattern_field_editor_example/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "indexPatternFieldEditorExample", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["data", "indexPatternFieldEditor", "developerExamples"], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/examples/index_pattern_field_editor_example/public/app.tsx b/examples/index_pattern_field_editor_example/public/app.tsx new file mode 100644 index 00000000000000..bd725759380aae --- /dev/null +++ b/examples/index_pattern_field_editor_example/public/app.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import ReactDOM from 'react-dom'; +import { + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiButton, + EuiInMemoryTable, + EuiText, + DefaultItemAction, +} from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { + DataPublicPluginStart, + IndexPattern, + IndexPatternField, +} from '../../../src/plugins/data/public'; +import { IndexPatternFieldEditorStart } from '../../../src/plugins/index_pattern_field_editor/public'; + +interface Props { + indexPattern?: IndexPattern; + indexPatternFieldEditor: IndexPatternFieldEditorStart; +} + +const IndexPatternFieldEditorExample = ({ indexPattern, indexPatternFieldEditor }: Props) => { + const [fields, setFields] = useState( + indexPattern?.getNonScriptedFields() || [] + ); + const refreshFields = () => setFields(indexPattern?.getNonScriptedFields() || []); + const columns = [ + { + field: 'name', + name: 'Field name', + }, + { + name: 'Actions', + actions: [ + { + name: 'Edit', + description: 'Edit this field', + icon: 'pencil', + type: 'icon', + 'data-test-subj': 'editField', + onClick: (fld: IndexPatternField) => + indexPatternFieldEditor.openEditor({ + ctx: { indexPattern: indexPattern! }, + fieldName: fld.name, + onSave: refreshFields, + }), + }, + { + name: 'Delete', + description: 'Delete this field', + icon: 'trash', + type: 'icon', + 'data-test-subj': 'deleteField', + available: (fld) => !!fld.runtimeField, + onClick: (fld: IndexPatternField) => + indexPatternFieldEditor.openDeleteModal({ + fieldName: fld.name, + ctx: { + indexPattern: indexPattern!, + }, + onDelete: refreshFields, + }), + }, + ] as Array>, + }, + ]; + + const content = indexPattern ? ( + <> + Index pattern: {indexPattern?.title} +
+ + indexPatternFieldEditor.openEditor({ + ctx: { indexPattern: indexPattern! }, + onSave: refreshFields, + }) + } + data-test-subj="addField" + > + Add field + +
+ + items={fields} + columns={columns} + pagination={true} + hasActions={true} + sorting={{ + sort: { + field: 'name', + direction: 'asc', + }, + }} + /> + + ) : ( +

Please create an index pattern

+ ); + + return ( + + + Index pattern field editor demo + + {content} + + + + ); +}; + +interface RenderAppDependencies { + data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; +} + +export const renderApp = async ( + { data, indexPatternFieldEditor }: RenderAppDependencies, + { element }: AppMountParameters +) => { + const indexPattern = (await data.indexPatterns.getDefault()) || undefined; + ReactDOM.render( + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/index_pattern_management/public/service/environment/index.ts b/examples/index_pattern_field_editor_example/public/index.ts similarity index 74% rename from src/plugins/index_pattern_management/public/service/environment/index.ts rename to examples/index_pattern_field_editor_example/public/index.ts index ab5297ed0e14c0..cc509da31d25fb 100644 --- a/src/plugins/index_pattern_management/public/service/environment/index.ts +++ b/examples/index_pattern_field_editor_example/public/index.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -export { EnvironmentService, Environment, EnvironmentServiceSetup } from './environment'; +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export const plugin = () => new IndexPatternFieldEditorPlugin(); diff --git a/examples/index_pattern_field_editor_example/public/plugin.tsx b/examples/index_pattern_field_editor_example/public/plugin.tsx new file mode 100644 index 00000000000000..ccbb93e3acf956 --- /dev/null +++ b/examples/index_pattern_field_editor_example/public/plugin.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { IndexPatternFieldEditorStart } from '../../../src/plugins/index_pattern_field_editor/public'; + +interface StartDeps { + data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; +} + +interface SetupDeps { + developerExamples: DeveloperExamplesSetup; +} + +export class IndexPatternFieldEditorPlugin implements Plugin { + public setup(core: CoreSetup, deps: SetupDeps) { + core.application.register({ + id: 'indexPatternFieldEditorExample', + title: 'Index pattern field editor example', + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + const [, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + return renderApp(depsStart, params); + }, + }); + + deps.developerExamples.register({ + appId: 'indexPatternFieldEditorExample', + title: 'Index pattern field editor', + description: `IndexPatternFieldEditor provides a UI for editing index pattern fields directly from Kibana apps. This example plugin demonstrates integration.`, + links: [ + { + label: 'README', + href: + 'https://github.com/elastic/kibana/blob/master/src/plugins/index_pattern_field_editor/README.md', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/index_pattern_field_editor_example/tsconfig.json b/examples/index_pattern_field_editor_example/tsconfig.json new file mode 100644 index 00000000000000..1f6d52ed5260e4 --- /dev/null +++ b/examples/index_pattern_field_editor_example/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + ] +} \ No newline at end of file diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 36d613ec82f9e0..d4ab8f624f7111 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -32,14 +32,14 @@ export class DocLinksService { guide: `${KIBANA_DOCS}canvas.html`, }, dashboard: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, - drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, - drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html#url-drilldowns`, - urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html`, - urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url_templating-language.html#url-template-variables`, + guide: `${KIBANA_DOCS}dashboard.html`, + drilldowns: `${KIBANA_DOCS}drilldowns.html`, + drilldownsTriggerPicker: `${KIBANA_DOCS}drilldowns.html#url-drilldowns`, + urlDrilldownTemplateSyntax: `${KIBANA_DOCS}url_templating-language.html`, + urlDrilldownVariables: `${KIBANA_DOCS}url_templating-language.html#url-template-variables`, }, discover: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/discover.html`, + guide: `${KIBANA_DOCS}discover.html`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, @@ -128,14 +128,14 @@ export class DocLinksService { luceneExpressions: `${ELASTICSEARCH_DOCS}modules-scripting-expression.html`, }, indexPatterns: { - introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, - fieldFormattersNumber: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/numeral.html`, - fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, + introduction: `${KIBANA_DOCS}index-patterns.html`, + fieldFormattersNumber: `${KIBANA_DOCS}numeral.html`, + fieldFormattersString: `${KIBANA_DOCS}field-formatters-string.html`, runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, }, - addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, - kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, - upgradeAssistant: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/upgrade-assistant.html`, + addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, + kibana: `${KIBANA_DOCS}index.html`, + upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, @@ -195,23 +195,23 @@ export class DocLinksService { }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, - kueryQuerySyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kuery-query.html`, + kueryQuerySyntax: `${KIBANA_DOCS}kuery-query.html`, luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`, percolate: `${ELASTICSEARCH_DOCS}query-dsl-percolate-query.html`, queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`, }, search: { - sessions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/search-sessions.html`, + sessions: `${KIBANA_DOCS}search-sessions.html`, }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, }, management: { - dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, + dashboardSettings: `${KIBANA_DOCS}advanced-options.html#kibana-dashboard-settings`, indexManagement: `${ELASTICSEARCH_DOCS}index-mgmt.html`, - kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, - visualizationSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-visualization-settings`, + kibanaSearchSettings: `${KIBANA_DOCS}advanced-options.html#kibana-search-settings`, + visualizationSettings: `${KIBANA_DOCS}advanced-options.html#kibana-visualization-settings`, }, ml: { guide: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/index.html`, @@ -242,52 +242,52 @@ export class DocLinksService { guide: `${ELASTICSEARCH_DOCS}transforms.html`, }, visualize: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html`, - timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/timelion.html`, + guide: `${KIBANA_DOCS}dashboard.html`, + timelionDeprecation: `${KIBANA_DOCS}timelion.html`, lens: `${ELASTIC_WEBSITE_URL}what-is/kibana-lens`, - lensPanels: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/lens.html`, + lensPanels: `${KIBANA_DOCS}lens.html`, maps: `${ELASTIC_WEBSITE_URL}maps`, - vega: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/vega.html`, + vega: `${KIBANA_DOCS}vega.html`, }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, }, alerting: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-management.html`, - actionTypes: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/action-types.html`, - emailAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, - emailActionConfig: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, - generalSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`, - indexAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html`, - esQuery: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-es-query.html`, - indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/rule-type-index-threshold.html`, - pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, - preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-connectors.html`, - preconfiguredAlertHistoryConnector: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html#preconfigured-connector-alert-history`, - serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`, - setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, - slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`, - teamsAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`, + guide: `${KIBANA_DOCS}alert-management.html`, + actionTypes: `${KIBANA_DOCS}action-types.html`, + emailAction: `${KIBANA_DOCS}email-action-type.html`, + emailActionConfig: `${KIBANA_DOCS}email-action-type.html`, + generalSettings: `${KIBANA_DOCS}alert-action-settings-kb.html#general-alert-action-settings`, + indexAction: `${KIBANA_DOCS}index-action-type.html`, + esQuery: `${KIBANA_DOCS}rule-type-es-query.html`, + indexThreshold: `${KIBANA_DOCS}rule-type-index-threshold.html`, + pagerDutyAction: `${KIBANA_DOCS}pagerduty-action-type.html`, + preconfiguredConnectors: `${KIBANA_DOCS}pre-configured-connectors.html`, + preconfiguredAlertHistoryConnector: `${KIBANA_DOCS}index-action-type.html#preconfigured-connector-alert-history`, + serviceNowAction: `${KIBANA_DOCS}servicenow-action-type.html#configuring-servicenow`, + setupPrerequisites: `${KIBANA_DOCS}alerting-getting-started.html#alerting-setup-prerequisites`, + slackAction: `${KIBANA_DOCS}slack-action-type.html#configuring-slack`, + teamsAction: `${KIBANA_DOCS}teams-action-type.html#configuring-teams`, }, maps: { - guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, - importGeospatialPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/import-geospatial-data.html#import-geospatial-privileges`, + guide: `${KIBANA_DOCS}maps.html`, + importGeospatialPrivileges: `${KIBANA_DOCS}import-geospatial-data.html#import-geospatial-privileges`, }, monitoring: { - alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`, - alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`, - alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`, - alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, - alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`, - alertsKibanaThreadpoolRejections: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-thread-pool-rejections`, - alertsKibanaCCRReadExceptions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, - alertsKibanaLargeShardSize: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-large-shard-size`, - alertsKibanaClusterAlerts: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cluster-alerts`, + alertsKibana: `${KIBANA_DOCS}kibana-alerts.html`, + alertsKibanaCpuThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-cpu-threshold`, + alertsKibanaDiskThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-disk-usage-threshold`, + alertsKibanaJvmThreshold: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-jvm-memory-threshold`, + alertsKibanaMissingData: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-missing-monitoring-data`, + alertsKibanaThreadpoolRejections: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-thread-pool-rejections`, + alertsKibanaCCRReadExceptions: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-ccr-read-exceptions`, + alertsKibanaLargeShardSize: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-large-shard-size`, + alertsKibanaClusterAlerts: `${KIBANA_DOCS}kibana-alerts.html#kibana-alerts-cluster-alerts`, metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`, monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`, - monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`, + monitorKibana: `${KIBANA_DOCS}monitoring-metricbeat.html`, monitorLogstash: `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html`, - troubleshootKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitor-troubleshooting.html`, + troubleshootKibana: `${KIBANA_DOCS}monitor-troubleshooting.html`, }, security: { apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`, @@ -295,8 +295,8 @@ export class DocLinksService { elasticsearchSettings: `${ELASTICSEARCH_DOCS}security-settings.html`, elasticsearchEnableSecurity: `${ELASTICSEARCH_DOCS}configuring-stack-security.html`, indicesPrivileges: `${ELASTICSEARCH_DOCS}security-privileges.html#privileges-list-indices`, - kibanaTLS: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`, - kibanaPrivileges: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-privileges.html`, + kibanaTLS: `${KIBANA_DOCS}configuring-tls.html`, + kibanaPrivileges: `${KIBANA_DOCS}kibana-privileges.html`, mappingRoles: `${ELASTICSEARCH_DOCS}mapping-roles.html`, mappingRolesFieldRules: `${ELASTICSEARCH_DOCS}role-mapping-resources.html#mapping-roles-rule-field`, runAsPrivilege: `${ELASTICSEARCH_DOCS}security-privileges.html#_run_as_privilege`, @@ -305,7 +305,7 @@ export class DocLinksService { jiraAction: `${ELASTICSEARCH_DOCS}actions-jira.html`, pagerDutyAction: `${ELASTICSEARCH_DOCS}actions-pagerduty.html`, slackAction: `${ELASTICSEARCH_DOCS}actions-slack.html`, - ui: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/watcher-ui.html`, + ui: `${KIBANA_DOCS}watcher-ui.html`, }, ccs: { guide: `${ELASTICSEARCH_DOCS}modules-cross-cluster-search.html`, diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index ce987e2870466b..0430614d413b6b 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -346,6 +346,7 @@ export function DiscoverLayout({ verticalPosition={contentCentered ? 'center' : undefined} horizontalPosition={contentCentered ? 'center' : undefined} paddingSize="none" + hasShadow={false} className={classNames('dscPageContent', { 'dscPageContent--centered': contentCentered, })} diff --git a/src/plugins/discover/public/application/helpers/columns.test.ts b/src/plugins/discover/public/application/helpers/columns.test.ts index df3884ab98bcff..6b6125193b5f37 100644 --- a/src/plugins/discover/public/application/helpers/columns.test.ts +++ b/src/plugins/discover/public/application/helpers/columns.test.ts @@ -43,4 +43,9 @@ describe('getDisplayedColumns', () => { ] `); }); + test('returns the same instance of ["_source"] over multiple calls', async () => { + const result = getDisplayedColumns([], indexPatternWithTimefieldMock); + const result2 = getDisplayedColumns([], indexPatternWithTimefieldMock); + expect(result).toBe(result2); + }); }); diff --git a/src/plugins/discover/public/application/helpers/columns.ts b/src/plugins/discover/public/application/helpers/columns.ts index 426059060f329b..6e77717c5cf054 100644 --- a/src/plugins/discover/public/application/helpers/columns.ts +++ b/src/plugins/discover/public/application/helpers/columns.ts @@ -8,6 +8,11 @@ import { IndexPattern } from '../../../../data/common'; +// We store this outside the function as a constant, so we're not creating a new array every time +// the function is returning this. A changing array might cause the data grid to think it got +// new columns, and thus performing worse than using the same array over multiple renders. +const SOURCE_ONLY = ['_source']; + /** * Function to provide fallback when * 1) no columns are given @@ -19,5 +24,5 @@ export function getDisplayedColumns(stateColumns: string[] = [], indexPattern: I // check if all columns where removed except the configured timeField (this can't be removed) !(stateColumns.length === 1 && stateColumns[0] === indexPattern.timeFieldName) ? stateColumns - : ['_source']; + : SOURCE_ONLY; } diff --git a/src/plugins/embeddable/common/lib/migrate.ts b/src/plugins/embeddable/common/lib/migrate.ts index 0a9cd0760eb2ce..fb8ea5cf2cd84e 100644 --- a/src/plugins/embeddable/common/lib/migrate.ts +++ b/src/plugins/embeddable/common/lib/migrate.ts @@ -26,9 +26,11 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) = updatedInput.enhancements = {}; Object.keys(enhancements).forEach((key) => { if (!enhancements[key]) return; - (updatedInput.enhancements! as Record)[key] = embeddables - .getEnhancement(key) - .migrations[version](enhancements[key] as SerializableState); + const enhancementDefinition = embeddables.getEnhancement(key); + const migratedEnhancement = enhancementDefinition?.migrations?.[version] + ? enhancementDefinition.migrations[version](enhancements[key] as SerializableState) + : enhancements[key]; + (updatedInput.enhancements! as Record)[key] = migratedEnhancement; }); return updatedInput; diff --git a/src/plugins/embeddable/public/plugin.test.ts b/src/plugins/embeddable/public/plugin.test.ts index 2e7d8c73cfc6da..53302e8e6870ce 100644 --- a/src/plugins/embeddable/public/plugin.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -184,4 +184,12 @@ describe('embeddable enhancements', () => { embeddableState.enhancements.test ); }); + + test('doesnt fail if there is no migration function registered for specific version', () => { + expect(() => { + start.migrate(embeddableState, '7.10.0'); + }).not.toThrow(); + + expect(start.migrate(embeddableState, '7.10.0')).toEqual(embeddableState); + }); }); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap index 645694371f9059..1310488c65fab8 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -59,7 +59,6 @@ exports[`EmptyState should render normally 1`] = ` } - isDisabled={false} onClick={[Function]} title={ { docLinks={docLinks} onRefresh={() => {}} navigateToApp={async () => {}} - getMlCardState={() => MlCardState.ENABLED} canSave={true} /> ); @@ -48,7 +46,6 @@ describe('EmptyState', () => { docLinks={docLinks} onRefresh={onRefreshHandler} navigateToApp={async () => {}} - getMlCardState={() => MlCardState.ENABLED} canSave={true} /> ); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx index 438eb8a031993f..240e732752916c 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -8,7 +8,6 @@ import './empty_state.scss'; import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DocLinksStart, ApplicationStart } from 'kibana/public'; import { @@ -28,60 +27,18 @@ import { } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public'; -import { MlCardState } from '../../../types'; export const EmptyState = ({ onRefresh, navigateToApp, docLinks, - getMlCardState, canSave, }: { onRefresh: () => void; navigateToApp: ApplicationStart['navigateToApp']; docLinks: DocLinksStart; - getMlCardState: () => MlCardState; canSave: boolean; }) => { - const mlCard = ( - - navigateToApp('ml', { path: '#/filedatavisualizer' })} - className="inpEmptyState__card" - betaBadgeLabel={ - getMlCardState() === MlCardState.ENABLED - ? undefined - : i18n.translate( - 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel', - { - defaultMessage: 'Basic', - } - ) - } - betaBadgeTooltipContent={i18n.translate( - 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription', - { - defaultMessage: 'This feature requires a Basic license.', - } - )} - isDisabled={getMlCardState() === MlCardState.DISABLED} - icon={} - title={ - - } - description={ - - } - /> - - ); - const createAnyway = ( - {getMlCardState() !== MlCardState.HIDDEN ? mlCard : <>} + + navigateToApp('home', { path: '#/tutorial_directory/fileDataViz' })} + className="inpEmptyState__card" + icon={} + title={ + + } + description={ + + } + /> + { application, http, data, - getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); const [creationOptions, setCreationOptions] = useState([]); @@ -182,7 +181,6 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { onRefresh={loadSources} docLinks={docLinks} navigateToApp={application.navigateToApp} - getMlCardState={getMlCardState} canSave={canSave} /> ); diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 94611705a93908..726c055d1b8c34 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -30,5 +30,3 @@ export { IndexPatternCreationOption, IndexPatternListConfig, } from './service'; - -export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 355f529fe0f759..ec5b7c74020a5a 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -23,7 +23,7 @@ import { CreateIndexPatternWizardWithRouter, } from '../components'; import { IndexPatternManagementStartDependencies, IndexPatternManagementStart } from '../plugin'; -import { IndexPatternManagmentContext, MlCardState } from '../types'; +import { IndexPatternManagmentContext } from '../types'; const readOnlyBadge = { text: i18n.translate('indexPatternManagement.indexPatterns.badge.readOnly.text', { @@ -37,8 +37,7 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, - params: ManagementAppMountParams, - getMlCardState: () => MlCardState + params: ManagementAppMountParams ) { const [ { chrome, application, uiSettings, notifications, overlays, http, docLinks }, @@ -63,7 +62,6 @@ export async function mountManagementSection( indexPatternFieldEditor, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, - getMlCardState, fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 3462131e50463b..6c709fb14f08d7 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -26,9 +26,6 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ list: { addListConfig: jest.fn(), } as any, - environment: { - update: jest.fn(), - }, }); const createStartContract = (): IndexPatternManagementStart => ({ @@ -93,7 +90,6 @@ const createIndexPatternManagmentContext = (): { indexPatternFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, - getMlCardState: () => 2, fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; }; diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ed92172c8b91ca..e3c156927bface 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -77,9 +77,7 @@ export class IndexPatternManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params, () => - this.indexPatternManagementService.environmentService.getEnvironment().ml() - ); + return mountManagementSection(core.getStartServices, params); }, }); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts deleted file mode 100644 index 1eaab2eaccc111..00000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { EnvironmentService, EnvironmentServiceSetup } from './environment'; -import { MlCardState } from '../../types'; - -const createSetupMock = (): jest.Mocked => { - const setup = { - update: jest.fn(), - }; - return setup; -}; - -const createMock = (): jest.Mocked> => { - const service = { - setup: jest.fn(), - getEnvironment: jest.fn(() => ({ - ml: () => MlCardState.ENABLED, - })), - }; - service.setup.mockImplementation(createSetupMock); - return service; -}; - -export const environmentServiceMock = { - createSetup: createSetupMock, - create: createMock, -}; diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts deleted file mode 100644 index 9e571374b4784f..00000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { EnvironmentService } from './environment'; -import { MlCardState } from '../../types'; - -describe('EnvironmentService', () => { - describe('setup', () => { - test('allows multiple update calls', () => { - const setup = new EnvironmentService().setup(); - expect(() => { - setup.update({ ml: () => MlCardState.ENABLED }); - }).not.toThrow(); - }); - }); - - describe('getEnvironment', () => { - test('returns default values', () => { - const service = new EnvironmentService(); - expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); - }); - - test('returns last state of update calls', () => { - let cardState = MlCardState.DISABLED; - const service = new EnvironmentService(); - const setup = service.setup(); - setup.update({ ml: () => cardState }); - expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); - cardState = MlCardState.ENABLED; - expect(service.getEnvironment().ml()).toEqual(MlCardState.ENABLED); - }); - }); -}); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.ts b/src/plugins/index_pattern_management/public/service/environment/environment.ts deleted file mode 100644 index 7bf0c1eb52068a..00000000000000 --- a/src/plugins/index_pattern_management/public/service/environment/environment.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { MlCardState } from '../../types'; - -/** @public */ -export interface Environment { - /** - * Flag whether ml features should be advertised - */ - readonly ml: () => MlCardState; -} - -export class EnvironmentService { - private environment = { - ml: () => MlCardState.DISABLED, - }; - - public setup() { - return { - /** - * Update the environment to influence how available features are presented. - * @param update - */ - update: (update: Partial) => { - this.environment = Object.assign({}, this.environment, update); - }, - }; - } - - public getEnvironment() { - return this.environment; - } -} - -export type EnvironmentServiceSetup = ReturnType; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index 15be7f11892e49..f30ccfcb9f3ed7 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -9,7 +9,6 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; -import { EnvironmentService } from './environment'; interface SetupDependencies { httpClient: HttpSetup; } @@ -22,12 +21,10 @@ interface SetupDependencies { export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; - environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); - this.environmentService = new EnvironmentService(); } public setup({ httpClient }: SetupDependencies) { @@ -40,7 +37,6 @@ export class IndexPatternManagementService { return { creation: creationManagerSetup, list: indexPatternListConfigSetup, - environment: this.environmentService.setup(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 58a138df633fd3..a61eeb99b25a57 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -33,14 +33,7 @@ export interface IndexPatternManagmentContext { indexPatternFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; - getMlCardState: () => MlCardState; fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; } export type IndexPatternManagmentContextValue = KibanaReactContextValue; - -export enum MlCardState { - HIDDEN, - DISABLED, - ENABLED, -} diff --git a/test/examples/config.js b/test/examples/config.js index cb6c487c564c3b..d47748e5f22a97 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -28,6 +28,7 @@ export default async function ({ readConfigFile }) { require.resolve('./state_sync'), require.resolve('./routing'), require.resolve('./expressions_explorer'), + require.resolve('./index_pattern_field_editor_example'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/index_pattern_field_editor_example/index.ts b/test/examples/index_pattern_field_editor_example/index.ts new file mode 100644 index 00000000000000..0cd23a33c84762 --- /dev/null +++ b/test/examples/index_pattern_field_editor_example/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const es = getService('es'); + const PageObjects = getPageObjects(['common', 'header', 'settings']); + + describe('index pattern field editor example', function () { + this.tags('ciGroup2'); + before(async () => { + await browser.setWindowSize(1300, 900); + await es.transport.request({ + path: '/blogs/_doc', + method: 'POST', + body: { user: 'matt', message: 20 }, + }); + + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern('blogs', null); + await PageObjects.common.navigateToApp('indexPatternFieldEditorExample'); + }); + + loadTestFile(require.resolve('./index_pattern_field_editor_example')); + }); +} diff --git a/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts b/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts new file mode 100644 index 00000000000000..5744c8e64f5c11 --- /dev/null +++ b/test/examples/index_pattern_field_editor_example/index_pattern_field_editor_example.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + + describe('', () => { + it('finds an index pattern', async () => { + await testSubjects.existOrFail('indexPatternTitle'); + }); + it('opens the field editor', async () => { + await testSubjects.click('addField'); + await testSubjects.existOrFail('flyoutTitle'); + }); + }); +} diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index c157765afb3590..6847b17bcef4b8 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -37,7 +37,7 @@ import { Alert, RecoveredActionGroup } from '../../common'; import { omit } from 'lodash'; import { UntypedNormalizedAlertType } from '../alert_type_registry'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; -import uuid from 'uuid'; + const alertType: jest.Mocked = { id: 'test', name: 'My test alert', @@ -373,6 +373,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -395,6 +397,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -525,6 +529,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -546,6 +552,8 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', + duration: 0, + start: '1970-01-01T00:00:00.000Z', }, kibana: { alerting: { @@ -669,7 +677,11 @@ describe('Task Runner', () => { meta: { lastScheduledActions: { date: '1970-01-01T00:00:00.000Z', group: 'default' }, }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 86400000000000, + }, }, }, }, @@ -699,6 +711,8 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -924,17 +938,221 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '1'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute-action", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "action_subgroup": undefined, + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + Object { + "id": "1", + "namespace": undefined, + "type": "action", + "type_id": "action", + }, + ], + }, + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + const runnerResult = await taskRunner.run(); + expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object { + "lastScheduledActions": Object { + "date": 1970-01-01T00:00:00.000Z, + "group": "default", + "subgroup": undefined, + }, + }, + "state": Object { + "bar": false, + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", + }, + }, + } + `); + + const logger = taskRunnerFactoryInitializerParams.logger; + expect(logger.debug).toHaveBeenCalledTimes(4); + expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); + expect(logger.debug).nthCalledWith( + 2, + `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` + ); + expect(logger.debug).nthCalledWith( + 3, + `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` + ); + expect(logger.debug).nthCalledWith( + 4, + 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' + ); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { "event": Object { - "action": "new-instance", + "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { "alerting": Object { - "action_group_id": "default", - "instance_id": "1", + "instance_id": "2", }, "saved_objects": Array [ Object { @@ -946,13 +1164,15 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' created new instance: '1'", + "message": "test:1: 'alert-name' instance '2' has recovered", }, ], Array [ Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -972,6 +1192,36 @@ describe('Task Runner', () => { "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], + Array [ + Object { + "event": Object { + "action": "execute-action", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "recovered", + "action_subgroup": undefined, + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + Object { + "id": "2", + "namespace": undefined, + "type": "action", + "type_id": "action", + }, + ], + }, + "message": "alert: test:1: 'alert-name' instanceId: '2' scheduled actionGroup: 'recovered' action: action:2", + }, + ], Array [ Object { "event": Object { @@ -1028,84 +1278,7 @@ describe('Task Runner', () => { ], ] `); - }); - - test('fire recovered actions for execution for the alertInstances which is in the recovered state', async () => { - taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); - taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); - - alertType.executor.mockImplementation( - async ({ - services: executorServices, - }: AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - string - >) => { - executorServices.alertInstanceFactory('1').scheduleActions('default'); - } - ); - const taskRunner = new TaskRunner( - alertType, - { - ...mockedTaskInstance, - state: { - ...mockedTaskInstance.state, - alertInstances: { - '1': { meta: {}, state: { bar: false } }, - '2': { meta: {}, state: { bar: false } }, - }, - }, - }, - taskRunnerFactoryInitializerParams - ); - alertsClient.get.mockResolvedValue(mockedAlertTypeSavedObject); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - const runnerResult = await taskRunner.run(); - expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object { - "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, - "group": "default", - "subgroup": undefined, - }, - }, - "state": Object { - "bar": false, - }, - }, - } - `); - - const logger = taskRunnerFactoryInitializerParams.logger; - expect(logger.debug).toHaveBeenCalledTimes(4); - expect(logger.debug).nthCalledWith(1, 'executing alert test:1 at 1970-01-01T00:00:00.000Z'); - expect(logger.debug).nthCalledWith( - 2, - `alert test:1: 'alert-name' has 1 active alert instances: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` - ); - expect(logger.debug).nthCalledWith( - 3, - `alert test:1: 'alert-name' has 1 recovered alert instances: [\"2\"]` - ); - expect(logger.debug).nthCalledWith( - 4, - 'alertExecutionStatus for test:1: {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"}' - ); - const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; - expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(2); expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -1129,7 +1302,7 @@ describe('Task Runner', () => { }); test('should skip alertInstances which werent active on the previous execution', async () => { - const alertId = uuid.v4(); + const alertId = 'e558aaad-fd81-46d2-96fc-3bd8fc3dc03f'; taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); @@ -1344,11 +1517,19 @@ describe('Task Runner', () => { alertInstances: { '1': { meta: { lastScheduledActions: { group: 'default', date } }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, }, '2': { meta: { lastScheduledActions: { group: 'default', date } }, - state: { bar: false }, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, }, }, }, @@ -1377,6 +1558,8 @@ describe('Task Runner', () => { }, "state": Object { "bar": false, + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, }, } @@ -1390,6 +1573,9 @@ describe('Task Runner', () => { Object { "event": Object { "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -1413,6 +1599,8 @@ describe('Task Runner', () => { Object { "event": Object { "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", }, "kibana": Object { "alerting": Object { @@ -2035,9 +2223,759 @@ describe('Task Runner', () => { references: [], }); + const logger = taskRunnerFactoryInitializerParams.logger; return taskRunner.run().catch((ex) => { expect(ex).toMatchInlineSnapshot(`[Error: Saved object [alert/1] not found]`); + expect(logger.debug).toHaveBeenCalledWith( + `Executing Alert "1" has resulted in Error: Saved object [alert/1] not found` + ); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).nthCalledWith( + 1, + `Unable to execute rule "1" because Saved object [alert/1] not found - this rule will not be rescheduled. To restart rule execution, try disabling and re-enabling this rule.` + ); expect(isUnrecoverableError(ex)).toBeTruthy(); }); }); + + test('correctly logs warning when Alert Task Runner throws due to failing to fetch the alert in a space', async () => { + alertsClient.get.mockImplementation(() => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1'); + }); + + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + params: { + ...mockedTaskInstance.params, + spaceId: 'test space', + }, + }, + taskRunnerFactoryInitializerParams + ); + + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const logger = taskRunnerFactoryInitializerParams.logger; + return taskRunner.run().catch((ex) => { + expect(ex).toMatchInlineSnapshot(`[Error: Saved object [alert/1] not found]`); + expect(logger.debug).toHaveBeenCalledWith( + `Executing Alert "1" has resulted in Error: Saved object [alert/1] not found` + ); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).nthCalledWith( + 1, + `Unable to execute rule "1" in the "test space" space because Saved object [alert/1] not found - this rule will not be rescheduled. To restart rule execution, try disabling and re-enabling this rule.` + ); + }); + }); + + test('start time is logged for new alerts', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: {}, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(5); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '1'", + }, + ], + Array [ + Object { + "event": Object { + "action": "new-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' created new instance: '2'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 0, + "start": "1970-01-01T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('duration is updated for active alerts when alert state contains start time', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 86400000000000, + "start": "1969-12-31T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + "duration": 64800000000000, + "start": "1969-12-31T06:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('duration is not calculated for active alerts when alert state does not contain start time', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => { + executorServices.alertInstanceFactory('1').scheduleActions('default'); + executorServices.alertInstanceFactory('2').scheduleActions('default'); + } + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { bar: false }, + }, + '2': { + meta: {}, + state: { bar: false }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "event": Object { + "action": "active-instance", + }, + "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' active instance: '2' in actionGroup: 'default'", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "active", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('end is logged for active alerts when alert state contains start time and alert recovers', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation(async () => {}); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T00:00:00.000Z', + duration: 80000000000, + }, + }, + '2': { + meta: {}, + state: { + bar: false, + start: '1969-12-31T06:00:00.000Z', + duration: 70000000000, + }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "recovered-instance", + "duration": 86400000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T00:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '1' has recovered", + }, + ], + Array [ + Object { + "event": Object { + "action": "recovered-instance", + "duration": 64800000000000, + "end": "1970-01-01T00:00:00.000Z", + "start": "1969-12-31T06:00:00.000Z", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '2' has recovered", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "ok", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); + + test('end calculation is skipped for active alerts when alert state does not contain start time and alert recovers', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + alertType.executor.mockImplementation( + async ({ + services: executorServices, + }: AlertExecutorOptions< + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, + string + >) => {} + ); + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: { + ...mockedTaskInstance.state, + alertInstances: { + '1': { + meta: {}, + state: { bar: false }, + }, + '2': { + meta: {}, + state: { bar: false }, + }, + }, + }, + }, + taskRunnerFactoryInitializerParams + ); + alertsClient.get.mockResolvedValue({ + ...mockedAlertTypeSavedObject, + notifyWhen: 'onActionGroupChange', + actions: [], + }); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + await taskRunner.run(); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(3); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "recovered-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "1", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '1' has recovered", + }, + ], + Array [ + Object { + "event": Object { + "action": "recovered-instance", + }, + "kibana": Object { + "alerting": Object { + "instance_id": "2", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "test:1: 'alert-name' instance '2' has recovered", + }, + ], + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "alerting": Object { + "status": "ok", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + "type_id": "test", + }, + ], + }, + "message": "alert executed: test:1: 'alert-name'", + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index fd82b38b493d79..4a214efceaa7d7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -323,6 +323,12 @@ export class TaskRunner< alertLabel, }); + trackAlertDurations({ + originalAlerts: originalAlertInstances, + currentAlerts: instancesWithScheduledActions, + recoveredAlerts: recoveredAlertInstances, + }); + generateNewAndRecoveredInstanceEvents({ eventLogger, originalAlertInstances, @@ -581,6 +587,10 @@ export class TaskRunner< ), schedule: resolveErr(schedule, (error) => { if (isAlertSavedObjectNotFoundError(error, alertId)) { + const spaceMessage = spaceId ? `in the "${spaceId}" space ` : ''; + this.logger.warn( + `Unable to execute rule "${alertId}" ${spaceMessage}because ${error.message} - this rule will not be rescheduled. To restart rule execution, try disabling and re-enabling this rule.` + ); throwUnrecoverableError(error); } return { interval: taskSchedule?.interval ?? FALLBACK_RETRY_INTERVAL }; @@ -589,6 +599,61 @@ export class TaskRunner< } } +interface TrackAlertDurationsParams< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +> { + originalAlerts: Dictionary>; + currentAlerts: Dictionary>; + recoveredAlerts: Dictionary>; +} + +function trackAlertDurations< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext +>(params: TrackAlertDurationsParams) { + const currentTime = new Date().toISOString(); + const { currentAlerts, originalAlerts, recoveredAlerts } = params; + const originalAlertIds = Object.keys(originalAlerts); + const currentAlertIds = Object.keys(currentAlerts); + const recoveredAlertIds = Object.keys(recoveredAlerts); + const newAlertIds = without(currentAlertIds, ...originalAlertIds); + + // Inject start time into instance state of new instances + for (const id of newAlertIds) { + const state = currentAlerts[id].getState(); + currentAlerts[id].replaceState({ ...state, start: currentTime }); + } + + // Calculate duration to date for active instances + for (const id of currentAlertIds) { + const state = originalAlertIds.includes(id) + ? originalAlerts[id].getState() + : currentAlerts[id].getState(); + const duration = state.start + ? (new Date(currentTime).valueOf() - new Date(state.start as string).valueOf()) * 1000 * 1000 // nanoseconds + : undefined; + currentAlerts[id].replaceState({ + ...state, + ...(state.start ? { start: state.start } : {}), + ...(duration !== undefined ? { duration } : {}), + }); + } + + // Inject end time into instance state of recovered instances + for (const id of recoveredAlertIds) { + const state = recoveredAlerts[id].getState(); + const duration = state.start + ? (new Date(currentTime).valueOf() - new Date(state.start as string).valueOf()) * 1000 * 1000 // nanoseconds + : undefined; + recoveredAlerts[id].replaceState({ + ...state, + ...(duration ? { duration } : {}), + ...(state.start ? { end: currentTime } : {}), + }); + } +} + interface GenerateNewAndRecoveredInstanceEventsParams< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext @@ -624,38 +689,66 @@ function generateNewAndRecoveredInstanceEvents< for (const id of recoveredAlertInstanceIds) { const { group: actionGroup, subgroup: actionSubgroup } = recoveredAlertInstances[id].getLastScheduledActions() ?? {}; + const state = recoveredAlertInstances[id].getState(); const message = `${params.alertLabel} instance '${id}' has recovered`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.recoveredInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.recoveredInstance, + message, + state, + actionGroup, + actionSubgroup + ); } for (const id of newIds) { const { actionGroup, subgroup: actionSubgroup } = currentAlertInstances[id].getScheduledActionOptions() ?? {}; + const state = currentAlertInstances[id].getState(); const message = `${params.alertLabel} created new instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.newInstance, + message, + state, + actionGroup, + actionSubgroup + ); } for (const id of currentAlertInstanceIds) { const { actionGroup, subgroup: actionSubgroup } = currentAlertInstances[id].getScheduledActionOptions() ?? {}; + const state = currentAlertInstances[id].getState(); const message = `${params.alertLabel} active instance: '${id}' in ${ actionSubgroup ? `actionGroup(subgroup): '${actionGroup}(${actionSubgroup})'` : `actionGroup: '${actionGroup}'` }`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.activeInstance, message, actionGroup, actionSubgroup); + logInstanceEvent( + id, + EVENT_LOG_ACTIONS.activeInstance, + message, + state, + actionGroup, + actionSubgroup + ); } function logInstanceEvent( instanceId: string, action: string, message: string, + state: InstanceState, group?: string, subgroup?: string ) { const event: IEvent = { event: { action, + ...(state?.start ? { start: state.start as string } : {}), + ...(state?.end ? { end: state.end as string } : {}), + ...(state?.duration !== undefined ? { duration: state.duration as number } : {}), }, kibana: { alerting: { diff --git a/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json b/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json index 113fba55553760..be5bc213e59a4a 100644 --- a/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json +++ b/x-pack/plugins/canvas/server/sample_data/ecommerce_saved_objects.json @@ -28,7 +28,7 @@ "height": 510, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" \n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"sum(maleCount) / count(maleCount)\" \n| revealImage origin=\"bottom\" image={asset \"asset-aaa14d64-2c1c-47f2-95c0-21306ee18cba\"} emptyImage={asset \"asset-960c8c6e-da72-412d-9d04-34a98cdb5760\"}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"male_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-aaa14d64-2c1c-47f2-95c0-21306ee18cba\"} emptyImage={asset \"asset-960c8c6e-da72-412d-9d04-34a98cdb5760\"}" }, { "id": "element-4a3fef74-5d8c-4bbe-8f3f-fe55afdd4b60", @@ -50,7 +50,7 @@ "height": 520, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\"\n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0}\n| math \"sum(maleCount) / count(maleCount)\"\n| revealImage origin=\"bottom\" image={asset \"asset-2f64bd10-953d-4163-90e9-a55e9ca4c52a\"} emptyImage={asset \"asset-3a26727a-b756-44be-a82c-273dd85bda09\"}", + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"female_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-2f64bd10-953d-4163-90e9-a55e9ca4c52a\"} emptyImage={asset \"asset-3a26727a-b756-44be-a82c-273dd85bda09\"}", "filter": null }, { @@ -194,7 +194,7 @@ "height": 56, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"femaleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0} | math \"round(100 * sum(femaleCount) / count(femaleCount))\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#eb6c66\" weight=\"normal\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * female_count / total_count)\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#eb6c66\" weight=\"normal\" underline=false italic=false}" }, { "id": "element-9e0b6230-2bc9-4995-8207-043e3063faeb", @@ -205,7 +205,7 @@ "height": 56, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"round(100 * sum(maleCount) / count(maleCount))\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#f8bd4a\" weight=\"normal\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * male_count / total_count)\" | markdown {context} \"%\" font={font family=\"Avenir\" size=48 align=\"center\" color=\"#f8bd4a\" weight=\"normal\" underline=false italic=false}" }, { "id": "element-2185edff-ac50-4162-b583-3bfd6469e925", @@ -216,7 +216,7 @@ "height": 721, "angle": 0 }, - "expression": "filters | demodata | markdown \"\" | render containerStyle={containerStyle backgroundColor=\"#ede9e7\"}" + "expression": "markdown \"\" | render containerStyle={containerStyle backgroundColor=\"#ede9e7\"}" }, { "id": "element-71b63f54-0961-4ed2-a85d-45584b48a631", @@ -535,7 +535,7 @@ "height": 202, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" \n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"sum(maleCount) / count(maleCount)\" \n| revealImage origin=\"bottom\" image={asset \"asset-803ec373-2608-4f6f-8cf9-0dbb2f6437ce\"} emptyImage={asset \"asset-18070a2a-cd01-410a-ba89-a4505e2fbc5b\"}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"male_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-803ec373-2608-4f6f-8cf9-0dbb2f6437ce\"} emptyImage={asset \"asset-18070a2a-cd01-410a-ba89-a4505e2fbc5b\"}" }, { "id": "element-2379c3ca-2c31-4948-8412-d14115500efc", @@ -546,7 +546,7 @@ "height": 202, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" \n| mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0} | math \"sum(maleCount) / count(maleCount)\" \n| revealImage origin=\"bottom\" image={asset \"asset-e644a484-4097-40b9-a08e-7250ba963059\"} emptyImage={asset \"asset-7e4f7119-b2d8-4527-9bd8-887cb25974e7\"}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"female_count / total_count\" \n| revealImage origin=\"bottom\" image={asset \"asset-e644a484-4097-40b9-a08e-7250ba963059\"} emptyImage={asset \"asset-7e4f7119-b2d8-4527-9bd8-887cb25974e7\"}" }, { "id": "element-3f52813f-7d0e-4ec7-9aad-c731b670d88d", @@ -964,7 +964,7 @@ "height": 63, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"femaleCount\" exp={getCell \"customer_gender\" | if {compare to=\"FEMALE\"} then=1 else=0} | math \"round(100 * sum(femaleCount) / count(femaleCount))\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#F05A24\" weight=\"bold\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='FEMALE' THEN 1 else 0 END) as female_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * female_count / total_count)\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#F05A24\" weight=\"bold\" underline=false italic=false}" }, { "id": "element-86b06b67-893e-4555-ad38-7fba9ea3153b", @@ -975,7 +975,7 @@ "height": 72, "angle": 0 }, - "expression": "esdocs index=\"kibana_sample_data_ecommerce\" sort=\"order_date, desc\" fields=\"customer_gender\" | mapColumn \"maleCount\" exp={getCell \"customer_gender\" | if {compare to=\"MALE\"} then=1 else=0} | math \"round(100 * sum(maleCount) / count(maleCount))\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#00A89C\" weight=\"bold\" underline=false italic=false}" + "expression": "essql query=\"select COUNT(*) as total_count, SUM(CASE WHEN customer_gender='MALE' THEN 1 else 0 END) as male_count from kibana_sample_data_ecommerce\"\n| math \"round(100 * male_count / total_count)\" | markdown {context} font={font family=\"nexa bold, Avenir\" size=48 align=\"center\" color=\"#00A89C\" weight=\"bold\" underline=false italic=false}" }, { "id": "element-507337d9-6e0e-4752-8770-6ebe88e9b3da", diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index 3934f0ee3417f7..b024a52e647218 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -19,6 +19,7 @@ "lens" ], "requiredBundles": [ + "home", "kibanaReact", "maps", "esUiShared" diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index 20d2e93fd68790..66109de1b14636 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -19,7 +19,7 @@ import type { SecurityPluginSetup } from '../../security/public'; import type { LensPublicStart } from '../../lens/public'; import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api'; import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; -import { registerHomeAddData } from './register_home'; +import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; export interface DataVisualizerSetupDependencies { home?: HomePublicPluginSetup; @@ -48,6 +48,7 @@ export class DataVisualizerPlugin public setup(core: CoreSetup, plugins: DataVisualizerSetupDependencies) { if (plugins.home) { registerHomeAddData(plugins.home); + registerHomeFeatureCatalogue(plugins.home); } } diff --git a/x-pack/plugins/data_visualizer/public/register_home.ts b/x-pack/plugins/data_visualizer/public/register_home.ts index 0b438dc309c4b1..3e8973784433ce 100644 --- a/x-pack/plugins/data_visualizer/public/register_home.ts +++ b/x-pack/plugins/data_visualizer/public/register_home.ts @@ -7,14 +7,34 @@ import { i18n } from '@kbn/i18n'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { FileDataVisualizerWrapper } from './lazy_load_bundle/component_wrapper'; +const FILE_DATA_VIS_TAB_ID = 'fileDataViz'; + export function registerHomeAddData(home: HomePublicPluginSetup) { home.addData.registerAddDataTab({ - id: 'fileDataViz', + id: FILE_DATA_VIS_TAB_ID, name: i18n.translate('xpack.dataVisualizer.file.embeddedTabTitle', { defaultMessage: 'Upload file', }), component: FileDataVisualizerWrapper, }); } + +export function registerHomeFeatureCatalogue(home: HomePublicPluginSetup) { + home.featureCatalogue.register({ + id: `file_data_visualizer`, + title: i18n.translate('xpack.dataVisualizer.title', { + defaultMessage: 'Upload a file', + }), + description: i18n.translate('xpack.dataVisualizer.description', { + defaultMessage: 'Import your own CSV, NDJSON, or log file.', + }), + icon: 'document', + path: `/app/home#/tutorial_directory/${FILE_DATA_VIS_TAB_ID}`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + order: 520, + }); +} diff --git a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html index 4e0c13442d2674..b2363ffbaa6411 100644 --- a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html +++ b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html @@ -9,4 +9,5 @@ initial-filter="initialFilter" initialPageSize="initialPageSize" core-start="coreStart" + class="kbnAppWrapper" > diff --git a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx index 6c2c79baafe0a5..7fa63404a4abd8 100644 --- a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx +++ b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx @@ -86,8 +86,8 @@ function GuidancePanelComponent(props: GuidancePanelProps) { const kibana = useKibana(); const { services, overlays } = kibana; - const { savedObjects, uiSettings, chrome, application } = services; - if (!overlays || !chrome || !application) return null; + const { savedObjects, uiSettings, application } = services; + if (!overlays || !application) return null; const onOpenDatasourcePicker = () => { openSourceModal({ overlays, savedObjects, uiSettings }, onIndexPatternSelected); @@ -149,8 +149,9 @@ function GuidancePanelComponent(props: GuidancePanelProps) { ); if (noIndexPatterns) { - const managementUrl = chrome.navLinks.get('kibana:stack_management')!.url; - const indexPatternUrl = `${managementUrl}/kibana/indexPatterns`; + const indexPatternUrl = application.getUrlForApp('management', { + path: '/kibana/indexPatterns', + }); const sampleDataUrl = `${application.getUrlForApp('home')}#/tutorial_directory/sampleData`; content = ( diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 92bac61f3fab3e..f34172765e1ddf 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,6 @@ "embeddable", "uiActions", "kibanaLegacy", - "indexPatternManagement", "discover", "triggersActionsUi" ], diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 42440883408ae5..1191f3b253fd7d 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -24,14 +24,12 @@ import type { } from 'src/plugins/share/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; -import type { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import type { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import type { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import type { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; import type { LicenseManagementUIPluginSetup } from '../../license_management/public'; import type { LicensingPluginSetup } from '../../licensing/public'; @@ -78,7 +76,6 @@ export interface MlSetupDependencies { uiActions: UiActionsSetup; kibanaVersion: string; share: SharePluginSetup; - indexPatternManagement: IndexPatternManagementSetup; triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup; alerting?: AlertingSetup; } @@ -148,12 +145,6 @@ export class MlPlugin implements Plugin { if (pluginsSetup.home) { registerFeature(pluginsSetup.home); } - - // register ML for the index pattern management no data screen. - pluginsSetup.indexPatternManagement.environment.update({ - ml: () => - capabilities.ml.canFindFileStructure ? MlCardState.ENABLED : MlCardState.HIDDEN, - }); } else { // if ml is disabled in elasticsearch, disable ML in kibana this.appUpdater$.next(() => ({ diff --git a/x-pack/plugins/ml/public/register_feature.ts b/x-pack/plugins/ml/public/register_feature.ts index 191e064c85b900..e8d64360df1230 100644 --- a/x-pack/plugins/ml/public/register_feature.ts +++ b/x-pack/plugins/ml/public/register_feature.ts @@ -13,10 +13,6 @@ import { import { PLUGIN_ID } from '../common/constants/app'; export const registerFeature = (home: HomePublicPluginSetup) => { - // register ML for the kibana home screen. - // so the file data visualizer appears to allow people to import data - home.environment.update({ ml: true }); - // register ML so it appears on the Kibana home page home.featureCatalogue.register({ id: PLUGIN_ID, @@ -37,19 +33,4 @@ export const registerFeature = (home: HomePublicPluginSetup) => { solutionId: 'kibana', order: 500, }); - - home.featureCatalogue.register({ - id: `${PLUGIN_ID}_file_data_visualizer`, - title: i18n.translate('xpack.ml.fileDataVisualizerTitle', { - defaultMessage: 'Upload a file', - }), - description: i18n.translate('xpack.ml.fileDataVisualizerDescription', { - defaultMessage: 'Import your own CSV, NDJSON, or log file.', - }), - icon: 'document', - path: '/app/ml/filedatavisualizer', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, - order: 520, - }); }; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json index a4412a6d732e99..3ff28ef3d8df3d 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/datafeed_high_count_network_denies.json @@ -13,10 +13,29 @@ "term": { "event.category": "network" } - }, + } + ], + "must": [ { - "term": { - "event.outcome": "deny" + "bool": { + "should": [ + { + "match": { + "event.outcome": { + "query": "deny", + "operator": "OR" + } + } + }, + { + "match": { + "event.type": { + "query": "denied", + "operator": "OR" + } + } + } + ] } } ] diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 3c9be9a823c498..33072e8df5cec5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -47,6 +47,7 @@ export interface HostIsolationResponse { export interface PendingActionsResponse { agent_id: string; pending_actions: { + /** Number of actions pending for each type. The `key` could be one of the `ISOLATION_ACTIONS` values. */ [key: string]: number; }; } diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx new file mode 100644 index 00000000000000..5cde22de697386 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/endpoint_host_isolation_status.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiBadge, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface EndpointHostIsolationStatusProps { + isIsolated: boolean; + /** the count of pending isolate actions */ + pendingIsolate?: number; + /** the count of pending unisoalte actions */ + pendingUnIsolate?: number; +} + +/** + * Component will display a host isoaltion status based on whether it is currently isolated or there are + * isolate/unisolate actions pending. If none of these are applicable, no UI component will be rendered + * (`null` is returned) + */ +export const EndpointHostIsolationStatus = memo( + ({ isIsolated, pendingIsolate = 0, pendingUnIsolate = 0 }) => { + return useMemo(() => { + // If nothing is pending and host is not currently isolated, then render nothing + if (!isIsolated && !pendingIsolate && !pendingUnIsolate) { + return null; + } + + // If nothing is pending, but host is isolated, then show isolation badge + if (!pendingIsolate && !pendingUnIsolate) { + return ( + + + + ); + } + + // If there are multiple types of pending isolation actions, then show count of actions with tooltip that displays breakdown + // TODO:PT implement edge case + // if () { + // + // } + + // Show 'pending [un]isolate' depending on what's pending + return ( + + + {pendingIsolate ? ( + + ) : ( + + )} + + + ); + }, [isIsolated, pendingIsolate, pendingUnIsolate]); + } +); + +EndpointHostIsolationStatus.displayName = 'EndpointHostIsolationStatus'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts index f5387a1b1a99c6..bd8e23e3a4559f 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts @@ -8,3 +8,4 @@ export * from './isolate_success'; export * from './isolate_form'; export * from './unisolate_form'; +export * from './endpoint_host_isolation_status'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 06b42145a6d48c..4d1ab0f3de8252 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -157,9 +157,9 @@ export const EndpointList = () => { const handleCreatePolicyClick = useNavigateToAppEventHandler( 'fleet', { - path: `#/policies/add-integration${ + path: `#/integrations/${ endpointPackageVersion ? `/endpoint-${endpointPackageVersion}` : '' - }`, + }/add-integration`, state: { onCancelNavigateTo: [ 'securitySolution:administration', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh new file mode 100755 index 00000000000000..d78726f9e23b5a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example delete all rules +# ./delete_rules_by_query.sh + +# Example delete rules with tag "test" +# ./delete_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"delete\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh new file mode 100755 index 00000000000000..1c7f05f4057cf1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/disable_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example disable all rules +# ./disable_rules_by_query.sh + +# Example disable rules with tag "test" +# ./disable_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"disable\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh new file mode 100755 index 00000000000000..13da480d74f83d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/duplicate_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example duplicate all rules +# ./duplicate_rules_by_query.sh + +# Example duplicate rules with tag "test" +# ./duplicate_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"duplicate\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh new file mode 100755 index 00000000000000..52bb51b6dd6dfb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/enable_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example enable all rules +# ./enable_rules_by_query.sh + +# Example enable rules with tag "test" +# ./enable_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"enable\" +}" | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh new file mode 100755 index 00000000000000..954fc2104262b6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/export_rules_by_query.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +set -e +./check_env_variables.sh + +QUERY=${1} + +# Example export all rules +# ./export_rules_by_query.sh + +# Example export rules with tag "test" +# ./export_rules_by_query.sh 'alert.attributes.tags: \"test\"' + +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_bulk_action \ + --data "{ + \"query\": \"$QUERY\", + \"action\": \"export\" +}" diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 460bbe2255a1e0..4bc03b4ba6e2aa 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2879,8 +2879,6 @@ "indexPatternManagement.createIndexPattern.betaLabel": "ベータ", "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", "indexPatternManagement.createIndexPattern.documentation": "ドキュメンテーションを表示", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "この機能にはベーシックライセンスが必要です。", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "新規データを確認", "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "一部のインデックスは表示されない場合があります。{link}してください。", "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "インデックスパターンを作成します", @@ -14658,8 +14656,6 @@ "xpack.ml.fieldTypeIcon.numberTypeAriaLabel": "数字タイプ", "xpack.ml.fieldTypeIcon.textTypeAriaLabel": "テキストタイプ", "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "不明なタイプ", - "xpack.ml.fileDataVisualizerDescription": "CSV、NDJSON、またはログファイルをインポートします。", - "xpack.ml.fileDataVisualizerTitle": "ファイルをアップロード", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "実際値が通常値と同じ", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "100x よりも高い", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "100x よりも低い", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f80ef442cbde0c..46bdb83c42528b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2893,8 +2893,6 @@ "indexPatternManagement.createIndexPattern.betaLabel": "公测版", "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", "indexPatternManagement.createIndexPattern.documentation": "阅读文档", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription": "此功能需要基本级许可证。", - "indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel": "基本级", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "检查新数据", "indexPatternManagement.createIndexPattern.emptyState.createAnyway": "部分索引可能已隐藏。仍然尝试{link}。", "indexPatternManagement.createIndexPattern.emptyState.createAnywayLink": "创建索引模式", @@ -14851,8 +14849,6 @@ "xpack.ml.fieldTypeIcon.numberTypeAriaLabel": "数字类型", "xpack.ml.fieldTypeIcon.textTypeAriaLabel": "文本类型", "xpack.ml.fieldTypeIcon.unknownTypeAriaLabel": "未知类型", - "xpack.ml.fileDataVisualizerDescription": "导入您自己的 CSV、NDJSON 或日志文件。", - "xpack.ml.fileDataVisualizerTitle": "上传文件", "xpack.ml.formatters.metricChangeDescription.actualSameAsTypicalDescription": "实际上与典型模式相同", "xpack.ml.formatters.metricChangeDescription.moreThan100xHigherDescription": "高 100 多倍", "xpack.ml.formatters.metricChangeDescription.moreThan100xLowerDescription": "低 100 多倍", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index e02cf44b0856eb..c34da1bc097bc0 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -12,7 +12,8 @@ import { PluginInitializerContext, AppMountParameters, } from 'kibana/public'; -import { of } from 'rxjs'; +import { from } from 'rxjs'; +import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -104,32 +105,41 @@ export class UptimePlugin }); plugins.observability.navigation.registerSections( - of([ - { - label: 'Uptime', - sortKey: 200, - entries: [ - { - label: i18n.translate('xpack.uptime.overview.heading', { - defaultMessage: 'Monitoring overview', - }), - app: 'uptime', - path: '/', - matchFullPath: true, - ignoreTrailingSlash: true, - }, - { - label: i18n.translate('xpack.uptime.certificatesPage.heading', { - defaultMessage: 'TLS Certificates', - }), - app: 'uptime', - path: '/certificates', - matchFullPath: true, - }, - ], - }, - ]) + from(core.getStartServices()).pipe( + map(([coreStart]) => { + if (coreStart.application.capabilities.uptime.show) { + return [ + { + label: 'Uptime', + sortKey: 200, + entries: [ + { + label: i18n.translate('xpack.uptime.overview.heading', { + defaultMessage: 'Monitoring overview', + }), + app: 'uptime', + path: '/', + matchFullPath: true, + ignoreTrailingSlash: true, + }, + { + label: i18n.translate('xpack.uptime.certificatesPage.heading', { + defaultMessage: 'TLS Certificates', + }), + app: 'uptime', + path: '/certificates', + matchFullPath: true, + }, + ], + }, + ]; + } + + return []; + }) + ) ); + core.application.register({ id: PLUGIN.ID, euiIconType: 'logoObservability', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index 40c0fe398bc57e..c1f6bcb9e15100 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -149,13 +149,17 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`); + validateInstanceEvent(event, `created new instance: 'instance'`, false); break; case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`); + validateInstanceEvent(event, `instance 'instance' has recovered`, true); break; case 'active-instance': - validateInstanceEvent(event, `active instance: 'instance' in actionGroup: 'default'`); + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup: 'default'`, + false + ); break; // this will get triggered as we add new event actions default: @@ -163,7 +167,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } } - function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { spaceId: Spaces.space1.id, savedObjects: [ @@ -172,6 +180,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, }); } }); @@ -288,10 +297,10 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); break; case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`); + validateInstanceEvent(event, `created new instance: 'instance'`, false); break; case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`); + validateInstanceEvent(event, `instance 'instance' has recovered`, true); break; case 'active-instance': expect( @@ -299,7 +308,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { ).to.be(true); validateInstanceEvent( event, - `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'` + `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, + false ); break; // this will get triggered as we add new event actions @@ -308,7 +318,11 @@ export default function eventLogTests({ getService }: FtrProviderContext) { } } - function validateInstanceEvent(event: IValidatedEvent, subMessage: string) { + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { spaceId: Spaces.space1.id, savedObjects: [ @@ -317,6 +331,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, }); } }); @@ -376,6 +391,7 @@ interface ValidateEventLogParams { savedObjects: SavedObject[]; outcome?: string; message: string; + shouldHaveEventEnd?: boolean; errorMessage?: string; status?: string; actionGroupId?: string; @@ -385,7 +401,7 @@ interface ValidateEventLogParams { export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { const { spaceId, savedObjects, outcome, message, errorMessage } = params; - const { status, actionGroupId, instanceId, reason } = params; + const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params; if (status) { expect(event?.kibana?.alerting?.status).to.be(status); @@ -411,16 +427,23 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa if (duration !== undefined) { expect(typeof duration).to.be('number'); expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); - const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) - ); + if (shouldHaveEventEnd !== false) { + expect(eventEnd).to.be.ok(); + + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); - // account for rounding errors - expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + } + + if (shouldHaveEventEnd === false) { + expect(eventEnd).not.to.be.ok(); + } } expect(event?.event?.outcome).to.equal(outcome); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts new file mode 100644 index 00000000000000..2b92562b9bde5d --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log_alerts.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server'; + +// eslint-disable-next-line import/no-default-export +export default function eventLogAlertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('eventLog alerts', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should generate expected alert events for normal operation', async () => { + // pattern of when the alert should fire + const pattern = { + instance: [false, true, true, false, false, true, true, true], + }; + + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [], + }) + ); + + expect(response.status).to.eql(200); + const ruleId = response.body.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); + + // wait for the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: Spaces.space1.id, + type: 'alert', + id: ruleId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute', { gte: 9 }], + ['new-instance', { equal: 2 }], + ['active-instance', { gte: 4 }], + ['recovered-instance', { equal: 2 }], + ]), + }); + }); + + // filter out the execute event actions + const instanceEvents = events.filter( + (event: IValidatedEvent) => event?.event?.action !== 'execute' + ); + + const currentAlertSpan: { + alertId?: string; + start?: string; + durationToDate?: number; + } = {}; + for (let i = 0; i < instanceEvents.length; ++i) { + switch (instanceEvents[i]?.event?.action) { + case 'new-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + // a new alert should generate a unique UUID for the duration of its activeness + expect(instanceEvents[i]?.event?.end).to.be(undefined); + + currentAlertSpan.alertId = instanceEvents[i]?.kibana?.alerting?.instance_id; + currentAlertSpan.start = instanceEvents[i]?.event?.start; + currentAlertSpan.durationToDate = instanceEvents[i]?.event?.duration; + break; + + case 'active-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); + expect(instanceEvents[i]?.event?.end).to.be(undefined); + + if (instanceEvents[i]?.event?.duration! !== 0) { + expect(instanceEvents[i]?.event?.duration! > currentAlertSpan.durationToDate!).to.be( + true + ); + } + currentAlertSpan.durationToDate = instanceEvents[i]?.event?.duration; + break; + + case 'recovered-instance': + expect(instanceEvents[i]?.kibana?.alerting?.instance_id).to.equal('instance'); + expect(instanceEvents[i]?.event?.start).to.equal(currentAlertSpan.start); + expect(instanceEvents[i]?.event?.end).not.to.be(undefined); + expect( + new Date(instanceEvents[i]?.event?.end!).valueOf() - + new Date(instanceEvents[i]?.event?.start!).valueOf() + ).to.equal(instanceEvents[i]?.event?.duration! / 1000 / 1000); + break; + } + } + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 9154c85af1bc7a..318dfdfe065dfe 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -76,7 +76,9 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); alertInstances.forEach(([key, value], index) => { expect(key).to.eql(`instance-${index}`); - expect(value.state).to.eql({ instanceStateValue: true }); + expect(value.state.instanceStateValue).to.be(true); + expect(value.state.start).not.to.be(undefined); + expect(value.state.duration).not.to.be(undefined); }); }); @@ -131,7 +133,9 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(alertInstances.length).to.eql(response.body.rule_type_state.runCount); alertInstances.forEach(([key, value], index) => { expect(key).to.eql(`instance-${index}`); - expect(value.state).to.eql({ instanceStateValue: true }); + expect(value.state.instanceStateValue).to.be(true); + expect(value.state.start).not.to.be(undefined); + expect(value.state.duration).not.to.be(undefined); }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index e9aeec1717c968..5c3374a4d9c704 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -37,6 +37,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./builtin_alert_types')); loadTestFile(require.resolve('./mustache_templates.ts')); loadTestFile(require.resolve('./notify_when')); + loadTestFile(require.resolve('./event_log_alerts')); // note that this test will destroy existing spaces loadTestFile(require.resolve('./migrations')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts index 0c274a8f4678bb..d5f287e78268eb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts @@ -47,8 +47,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 - describe.skip('Finalizing signals migrations', () => { + describe('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts index 6bb00c55f690e1..5c726a177bb601 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/get_signals_migration_status.ts @@ -19,8 +19,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/99915 - describe.skip('Signals migration status', () => { + describe('Signals migration status', () => { let legacySignalsIndexName: string; beforeEach(async () => { await createSignalsIndex(supertest); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index bbc7f5992506b3..18f4f6a38a7b10 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -35,14 +35,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 280801d1becb5b..431c0550b92718 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -41,16 +41,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.error.expectForbidden(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should not display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.navigateToKibanaHome(); await ml.navigation.openKibanaNav(); await ml.testExecution.logTestStep('should not display the ML nav link'); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index dbf467e998f251..a53ed2fafe30c7 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -35,14 +35,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 31cf17575bdd9a..2de5d83714aeed 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -98,14 +98,6 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte }); }, - async assertKibanaHomeFileDataVisLinkExists() { - await testSubjects.existOrFail('homeSynopsisLinkml_file_data_visualizer'); - }, - - async assertKibanaHomeFileDataVisLinkNotExists() { - await testSubjects.missingOrFail('homeSynopsisLinkml_file_data_visualizer'); - }, - async assertRadioGroupValue(testSubject: string, expectedValue: string) { const assertRadioGroupValue = await testSubjects.find(testSubject); const input = await assertRadioGroupValue.findByCssSelector(':checked'); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts index 7fdfbb45269c30..aff1402f5567ee 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/full_ml_access.ts @@ -57,14 +57,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts index 91a37d0d98cda3..8d3aa3c6b6ada7 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts @@ -39,16 +39,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.error.expectForbidden(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should not display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.navigateToKibanaHome(); await ml.navigation.openKibanaNav(); await ml.testExecution.logTestStep('should not display the ML nav link'); diff --git a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts index e58e46e985fd9a..2e5216d7225186 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/read_ml_access.ts @@ -58,14 +58,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.logout(); }); - it('should not display the ML file data vis link on the Kibana home page', async () => { - await ml.testExecution.logTestStep('should load the Kibana home page'); - await ml.navigation.navigateToKibanaHome(); - - await ml.testExecution.logTestStep('should not display the ML file data vis link'); - await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); - }); - it('should display the ML entry in Kibana app menu', async () => { await ml.testExecution.logTestStep('should open the Kibana app menu'); await ml.navigation.openKibanaNav(); diff --git a/yarn.lock b/yarn.lock index 3afbffbac6eb1b..167a8500a26eaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29273,16 +29273,16 @@ write@1.0.3: mkdirp "^0.5.1" ws@^6.1.2, ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== dependencies: async-limiter "~1.0.0" ws@^7.2.3: - version "7.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + version "7.4.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" + integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== x-is-function@^1.0.4: version "1.0.4"