diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 40be42461c493..5340b4bf578cd 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -162,10 +162,12 @@ enabled: - x-pack/test/functional/apps/maps/group2/config.ts - x-pack/test/functional/apps/maps/group3/config.ts - x-pack/test/functional/apps/maps/group4/config.ts + - x-pack/test/functional/apps/ml/anomaly_detection/config.ts + - x-pack/test/functional/apps/ml/data_frame_analytics/config.ts - x-pack/test/functional/apps/ml/data_visualizer/config.ts - - x-pack/test/functional/apps/ml/group1/config.ts - - x-pack/test/functional/apps/ml/group2/config.ts - - x-pack/test/functional/apps/ml/group3/config.ts + - x-pack/test/functional/apps/ml/permissions/config.ts + - x-pack/test/functional/apps/ml/short_tests/config.ts + - x-pack/test/functional/apps/ml/stack_management_jobs/config.ts - x-pack/test/functional/apps/monitoring/config.ts - x-pack/test/functional/apps/remote_clusters/config.ts - x-pack/test/functional/apps/reporting_management/config.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 156a306b12e89..e98ba1d451ff3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,6 +83,7 @@ /x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services /x-pack/plugins/runtime_fields @elastic/kibana-app-services /x-pack/test/search_sessions_integration/ @elastic/kibana-app-services +/src/plugins/dashboard/public/application/embeddable/viewport/print_media @elastic/kibana-app-services ### Observability Plugins diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 63e104c44b173..a91776fde65ba 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -222,7 +222,7 @@ It also provides a stateful version of it on the start contract. |{kib-repo}blob/{branch}/src/plugins/newsfeed/README.md[newsfeed] |The newsfeed plugin adds a NewsfeedNavButton to the top navigation bar and renders the content in the flyout. -Content is fetched from the remote (https://feeds.elastic.co and https://feeds-staging.elastic.co in dev mode) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely. +Content is fetched from the remote (https://feeds.elastic.co) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely. |{kib-repo}blob/{branch}/src/plugins/presentation_util/README.mdx[presentationUtil] diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 7441621f441f9..2cfd3169b45a3 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -38,71 +38,69 @@ If you'd like to change any of the default values, copy and paste the relevant settings into your `kibana.yml` configuration file. Changing these settings may disable features of the APM App. -[cols="2*<"] -|=== -| `xpack.apm.maxServiceEnvironments` {ess-icon} - | Maximum number of unique service environments recognized by the UI. Defaults to `100`. +`xpack.apm.maxServiceEnvironments` {ess-icon}:: +Maximum number of unique service environments recognized by the UI. Defaults to `100`. -| `xpack.apm.serviceMapFingerprintBucketSize` {ess-icon} - | Maximum number of unique transaction combinations sampled for generating service map focused on a specific service. Defaults to `100`. +`xpack.apm.serviceMapFingerprintBucketSize` {ess-icon}:: +Maximum number of unique transaction combinations sampled for generating service map focused on a specific service. Defaults to `100`. -| `xpack.apm.serviceMapFingerprintGlobalBucketSize` {ess-icon} - | Maximum number of unique transaction combinations sampled for generating the global service map. Defaults to `100`. +`xpack.apm.serviceMapFingerprintGlobalBucketSize` {ess-icon}:: +Maximum number of unique transaction combinations sampled for generating the global service map. Defaults to `100`. -| `xpack.apm.serviceMapEnabled` {ess-icon} - | Set to `false` to disable service maps. Defaults to `true`. +`xpack.apm.serviceMapEnabled` {ess-icon}:: +Set to `false` to disable service maps. Defaults to `true`. -| `xpack.apm.serviceMapTraceIdBucketSize` {ess-icon} - | Maximum number of trace IDs sampled for generating service map focused on a specific service. Defaults to `65`. +`xpack.apm.serviceMapTraceIdBucketSize` {ess-icon}:: +Maximum number of trace IDs sampled for generating service map focused on a specific service. Defaults to `65`. -| `xpack.apm.serviceMapTraceIdGlobalBucketSize` {ess-icon} - | Maximum number of trace IDs sampled for generating the global service map. Defaults to `6`. +`xpack.apm.serviceMapTraceIdGlobalBucketSize` {ess-icon}:: +Maximum number of trace IDs sampled for generating the global service map. Defaults to `6`. -| `xpack.apm.serviceMapMaxTracesPerRequest` {ess-icon} - | Maximum number of traces per request for generating the global service map. Defaults to `50`. +`xpack.apm.serviceMapMaxTracesPerRequest` {ess-icon}:: +Maximum number of traces per request for generating the global service map. Defaults to `50`. -| `xpack.apm.ui.enabled` {ess-icon} - | Set to `false` to hide the APM app from the main menu. Defaults to `true`. +`xpack.apm.ui.enabled` {ess-icon}:: +Set to `false` to hide the APM app from the main menu. Defaults to `true`. -| `xpack.apm.ui.transactionGroupBucketSize` {ess-icon} - | Number of top transaction groups displayed in the APM app. Defaults to `1000`. +`xpack.apm.ui.transactionGroupBucketSize` {ess-icon}:: +Number of top transaction groups displayed in the APM app. Defaults to `1000`. -| `xpack.apm.ui.maxTraceItems` {ess-icon} - | Maximum number of child items displayed when viewing trace details. Defaults to `1000`. +`xpack.apm.ui.maxTraceItems` {ess-icon}:: +Maximum number of child items displayed when viewing trace details. Defaults to `1000`. -| `xpack.observability.annotations.index` {ess-icon} - | Index name where Observability annotations are stored. Defaults to `observability-annotations`. +`xpack.observability.annotations.index` {ess-icon}:: +Index name where Observability annotations are stored. Defaults to `observability-annotations`. -| `xpack.apm.searchAggregatedTransactions` {ess-icon} - | Enables Transaction histogram metrics. Defaults to `auto` so the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. When set to `never` and aggregated transactions are not used. - See {apm-guide-ref}/transaction-metrics.html[Configure transaction metrics] for more information. +`xpack.apm.searchAggregatedTransactions` {ess-icon}:: +Enables Transaction histogram metrics. Defaults to `auto` so the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. When set to `never` and aggregated transactions are not used. ++ +See {apm-guide-ref}/transaction-metrics.html[Configure transaction metrics] for more information. -| `xpack.apm.metricsInterval` {ess-icon} - | Sets a `fixed_interval` for date histograms in metrics aggregations. Defaults to `30`. +`xpack.apm.metricsInterval` {ess-icon}:: +Sets a `fixed_interval` for date histograms in metrics aggregations. Defaults to `30`. -| `xpack.apm.agent.migrations.enabled` {ess-icon} - | Set to `false` to disable cloud APM migrations. Defaults to `true`. +`xpack.apm.agent.migrations.enabled` {ess-icon}:: +Set to `false` to disable cloud APM migrations. Defaults to `true`. -| `xpack.apm.indices.error` {ess-icon} - | Matcher for all error indices. Defaults to `logs-apm*,apm-*`. +`xpack.apm.indices.error` {ess-icon}:: +Matcher for all error indices. Defaults to `logs-apm*,apm-*`. -| `xpack.apm.indices.onboarding` {ess-icon} - | Matcher for all onboarding indices. Defaults to `apm-*`. +`xpack.apm.indices.onboarding` {ess-icon}:: +Matcher for all onboarding indices. Defaults to `apm-*`. -| `xpack.apm.indices.span` {ess-icon} - | Matcher for all span indices. Defaults to `traces-apm*,apm-*`. +`xpack.apm.indices.span` {ess-icon}:: +Matcher for all span indices. Defaults to `traces-apm*,apm-*`. -| `xpack.apm.indices.transaction` {ess-icon} - | Matcher for all transaction indices. Defaults to `traces-apm*,apm-*`. +`xpack.apm.indices.transaction` {ess-icon}:: +Matcher for all transaction indices. Defaults to `traces-apm*,apm-*`. -| `xpack.apm.indices.metric` {ess-icon} - | Matcher for all metrics indices. Defaults to `metrics-apm*,apm-*`. +`xpack.apm.indices.metric` {ess-icon}:: +Matcher for all metrics indices. Defaults to `metrics-apm*,apm-*`. -| `xpack.apm.indices.sourcemap` {ess-icon} - | Matcher for all source map indices. Defaults to `apm-*`. +`xpack.apm.indices.sourcemap` {ess-icon}:: +Matcher for all source map indices. Defaults to `apm-*`. -| `xpack.apm.autoCreateApmDataView` {ess-icon} - | Set to `false` to disable the automatic creation of the APM data view when the APM app is opened. Defaults to `true`. -|=== +`xpack.apm.autoCreateApmDataView` {ess-icon}:: +Set to `false` to disable the automatic creation of the APM data view when the APM app is opened. Defaults to `true`. // end::general-apm-settings[] diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc index 5ddf45887a530..ddce9feb3e640 100644 --- a/docs/settings/fleet-settings.asciidoc +++ b/docs/settings/fleet-settings.asciidoc @@ -18,104 +18,140 @@ See the {fleet-guide}/index.html[{fleet}] docs for more information. [[general-fleet-settings-kb]] ==== General {fleet} settings -[cols="2*<"] -|=== -| `xpack.fleet.agents.enabled` {ess-icon} - | Set to `true` (default) to enable {fleet}. -|=== +`xpack.fleet.agents.enabled` {ess-icon}:: +Set to `true` (default) to enable {fleet}. + [[fleet-data-visualizer-settings]] ==== {package-manager} settings -[cols="2*<"] -|=== -| `xpack.fleet.registryUrl` - | The address to use to reach the {package-manager} registry. -| `xpack.fleet.registryProxyUrl` - | The proxy address to use to reach the {package-manager} registry if an internet connection is not directly available. - Refer to {fleet-guide}/air-gapped.html[Air-gapped environments] for details. +`xpack.fleet.registryUrl`:: +The address to use to reach the {package-manager} registry. + +`xpack.fleet.registryProxyUrl`:: +The proxy address to use to reach the {package-manager} registry if an internet connection is not directly available. +Refer to {fleet-guide}/air-gapped.html[Air-gapped environments] for details. -|=== ==== {fleet} settings -[cols="2*<"] -|=== -| `xpack.fleet.agents.fleet_server.hosts` - | Hostnames used by {agent} for accessing {fleet-server}. -| `xpack.fleet.agents.elasticsearch.hosts` - | Hostnames used by {agent} for accessing {es}. -| `xpack.fleet.agents.elasticsearch.ca_sha256` - | Hash pin used for certificate verification. The pin is a base64-encoded - string of the SHA-256 fingerprint. -|=== +`xpack.fleet.agents.fleet_server.hosts`:: +Hostnames used by {agent} for accessing {fleet-server}. + +`xpack.fleet.agents.elasticsearch.hosts`:: +Hostnames used by {agent} for accessing {es}. +`xpack.fleet.agents.elasticsearch.ca_sha256`:: +Hash pin used for certificate verification. The pin is a base64-encoded string of the SHA-256 fingerprint. + +[role="child_attributes"] ==== Preconfiguration settings (for advanced use cases) Use these settings to pre-define integrations and agent policies that you want {fleet} to load up by default. -[cols="2* {}; interface RunOptions extends ProcOptions { wait: true | RegExp; waitTimeout?: number | false; + onEarlyExit?: (msg: string) => void; } /** @@ -47,16 +48,6 @@ export class ProcRunner { /** * Start a process, tracking it by `name` - * @param {String} name - * @param {Object} options - * @property {String} options.cmd executable to run - * @property {Array?} options.args arguments to provide the executable - * @property {String?} options.cwd current working directory for the process - * @property {RegExp|Boolean} options.wait Should start() wait for some time? Use - * `true` will wait until the proc exits, - * a `RegExp` will wait until that log line - * is found - * @return {Promise} */ async run(name: string, options: RunOptions) { const { @@ -66,6 +57,7 @@ export class ProcRunner { wait = false, waitTimeout = 15 * MINUTE, env = process.env, + onEarlyExit, } = options; const cmd = options.cmd === 'node' ? process.execPath : options.cmd; @@ -89,6 +81,25 @@ export class ProcRunner { stdin, }); + if (onEarlyExit) { + proc.outcomePromise + .then( + (code) => { + if (!proc.stopWasCalled()) { + onEarlyExit(`[${name}] exitted early with ${code}`); + } + }, + (error) => { + if (!proc.stopWasCalled()) { + onEarlyExit(`[${name}] exitted early: ${error.message}`); + } + } + ) + .catch((error) => { + throw new Error(`Error handling early exit: ${error.stack}`); + }); + } + try { if (wait instanceof RegExp) { // wait for process to log matching line diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index 899a7843a68fc..cf871abe6f18f 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -33,7 +33,7 @@ export function runCli() { string: ['es-url', 'kibana-url', 'config', 'es-ca', 'kibana-ca'], help: ` --config path to an FTR config file that sets --es-url and --kibana-url - default: ${defaultConfigPath} + default: ${Path.relative(process.cwd(), defaultConfigPath)} --es-url url for Elasticsearch, prefer the --config flag --kibana-url url for Kibana, prefer the --config flag --kibana-ca if Kibana url points to https://localhost we default to the CA from @kbn/dev-utils, customize the CA with this flag diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index 50ca9fa91e0aa..eecaef06be453 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -215,6 +215,25 @@ exports.Cluster = class Cluster { }), ]); }); + + if (options.onEarlyExit) { + this._outcome + .then( + () => { + if (!this._stopCalled) { + options.onEarlyExit(`ES exitted unexpectedly`); + } + }, + (error) => { + if (!this._stopCalled) { + options.onEarlyExit(`ES exitted unexpectedly: ${error.stack}`); + } + } + ) + .catch((error) => { + throw new Error(`failure handling early exit: ${error.stack}`); + }); + } } /** diff --git a/packages/kbn-es/src/cluster_exec_options.ts b/packages/kbn-es/src/cluster_exec_options.ts index 8ef3b23cd8c51..da21aaf05b139 100644 --- a/packages/kbn-es/src/cluster_exec_options.ts +++ b/packages/kbn-es/src/cluster_exec_options.ts @@ -15,4 +15,5 @@ export interface EsClusterExecOptions { password?: string; skipReadyCheck?: boolean; readyTimeout?: number; + onEarlyExit?: (msg: string) => void; } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 9baed7a92a53e..0e7d4939cbc29 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -104,7 +104,7 @@ pageLoadAssetSize: fieldFormats: 65209 kibanaReact: 74422 share: 71239 - uiActions: 35121 + uiActions: 35121 embeddable: 87309 embeddableEnhanced: 22107 uiActionsEnhanced: 38494 @@ -128,3 +128,4 @@ pageLoadAssetSize: screenshotting: 22870 synthetics: 40958 expressionXY: 29000 + kibanaUsageCollection: 16463 diff --git a/packages/kbn-shared-ux-components/BUILD.bazel b/packages/kbn-shared-ux-components/BUILD.bazel index 8eca4da014493..b1420f5376041 100644 --- a/packages/kbn-shared-ux-components/BUILD.bazel +++ b/packages/kbn-shared-ux-components/BUILD.bazel @@ -40,8 +40,10 @@ NPM_MODULE_EXTRA_FILES = [ # "@npm//name-of-package" # eg. "@npm//lodash" RUNTIME_DEPS = [ - "//packages/kbn-i18n", "//packages/kbn-i18n-react", + "//packages/kbn-i18n", + "//packages/shared-ux/avatar/solution", + "//packages/shared-ux/link/redirect_app", "//packages/kbn-shared-ux-services", "//packages/kbn-shared-ux-storybook", "//packages/kbn-shared-ux-utility", @@ -51,6 +53,7 @@ RUNTIME_DEPS = [ "@npm//classnames", "@npm//react-use", "@npm//react", + "@npm//rxjs", "@npm//url-loader", ] @@ -64,12 +67,14 @@ RUNTIME_DEPS = [ # # References to NPM packages work the same as RUNTIME_DEPS TYPES_DEPS = [ - "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-ambient-ui-types", "//packages/kbn-i18n-react:npm_module_types", + "//packages/kbn-i18n:npm_module_types", + "//packages/shared-ux/avatar/solution:npm_module_types", + "//packages/shared-ux/link/redirect_app:npm_module_types", "//packages/kbn-shared-ux-services:npm_module_types", "//packages/kbn-shared-ux-storybook:npm_module_types", "//packages/kbn-shared-ux-utility:npm_module_types", - "//packages/kbn-ambient-ui-types", "@npm//@types/node", "@npm//@types/jest", "@npm//@types/react", @@ -78,6 +83,7 @@ TYPES_DEPS = [ "@npm//@emotion/css", "@npm//@elastic/eui", "@npm//react-use", + "@npm//rxjs", ] jsts_transpiler( diff --git a/packages/kbn-shared-ux-components/src/index.ts b/packages/kbn-shared-ux-components/src/index.ts index 05afc94f782c8..77586e8592b6a 100644 --- a/packages/kbn-shared-ux-components/src/index.ts +++ b/packages/kbn-shared-ux-components/src/index.ts @@ -15,8 +15,6 @@ export const LazyToolbarButton = React.lazy(() => })) ); -export const RedirectAppLinks = React.lazy(() => import('./redirect_app_links')); - /** * A `ToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can * be used directly by consumers and will load the `LazyToolbarButton` component lazily with @@ -100,23 +98,6 @@ export const KibanaPageTemplateSolutionNavLazy = React.lazy(() => */ export const KibanaPageTemplateSolutionNav = withSuspense(KibanaPageTemplateSolutionNavLazy); -/** - * The Lazily-loaded `KibanaSolutionAvatar` component. Consumers should use `React.Suspense` or - * the withSuspense` HOC to load this component. - */ -export const KibanaSolutionAvatarLazy = React.lazy(() => - import('./solution_avatar').then(({ KibanaSolutionAvatar }) => ({ - default: KibanaSolutionAvatar, - })) -); - -/** - * A `KibanaSolutionAvatar` component that is wrapped by the `withSuspense` HOC. This component can - * be used directly by consumers and will load the `KibanaPageTemplateSolutionNavAvatarLazy` component lazily with - * a predefined fallback and error boundary. - */ -export const KibanaSolutionAvatar = withSuspense(KibanaSolutionAvatarLazy); - /** * The Lazily-loaded `NoDataViews` component. Consumers should use `React.Suspennse` or the * `withSuspense` HOC to load this component. diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap index 66b085b284391..0046e9c3fd3c1 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap @@ -7,7 +7,7 @@ exports[`NoDataPage render 1`] = ` - - - + `; exports[`ElasticAgentCardComponent props href 1`] = ` - - - + `; exports[`ElasticAgentCardComponent renders 1`] = ` - - - + `; exports[`ElasticAgentCardComponent renders with canAccessFleet false 1`] = ` - + This integration is not yet enabled. Your administrator has the required permissions to turn it on. + + } + image="test-file-stub" + isDisabled={true} + title={ + + Contact your administrator + } - navigateToUrl={[MockFunction]} -> - - This integration is not yet enabled. Your administrator has the required permissions to turn it on. - - } - image="test-file-stub" - isDisabled={true} - title={ - - Contact your administrator - - } - /> - +/> `; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index 79c0ea245b6cb..b15f254a5274a 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -4,7 +4,9 @@ exports[`ElasticAgentCard renders 1`] = ` - - -
- + - - Add Elastic Agent - - } - href="/app/integrations/browse" - image="test-file-stub" - paddingSize="l" - title="Add Elastic Agent" +
- - - - , - ], - }, + + + Add Elastic Agent + } - } - /> - -
-
-
- -
-
-
- - - - Add Elastic Agent - - - - + + , + ], + }, + } + } + isStringTag={false} + serialized={ + Object { + "map": undefined, + "name": "1hu4pg0-EuiCard", + "next": undefined, + "styles": "max-width:400px;margin-inline:auto;;label:EuiCard;", + "toString": [Function], + } + } + /> +
-

- Use Elastic Agent for a simple, unified way to collect data from your machines. -

-
-
-
-
- - -
+
- - Add Elastic Agent - + - - - - -
-
-
-
- - -
-
- + + +
+

+ Use Elastic Agent for a simple, unified way to collect data from your machines. +

+
+
+
+
+ + + + + +
+ + + + +
+ + + + + + `; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.test.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.test.tsx index f25edb069c629..367fcd10b96a9 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.test.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.test.tsx @@ -10,31 +10,15 @@ import { shallow } from 'enzyme'; import React from 'react'; import { ElasticAgentCardComponent } from './elastic_agent_card.component'; import { NoDataCard } from './no_data_card'; -import { Subject } from 'rxjs'; describe('ElasticAgentCardComponent', () => { - const navigateToUrl = jest.fn(); - const currentAppId$ = new Subject().asObservable(); - test('renders', () => { - const component = shallow( - - ); + const component = shallow(); expect(component).toMatchSnapshot(); }); test('renders with canAccessFleet false', () => { - const component = shallow( - - ); + const component = shallow(); expect(component.find(NoDataCard).props().isDisabled).toBe(true); expect(component).toMatchSnapshot(); }); @@ -42,12 +26,7 @@ describe('ElasticAgentCardComponent', () => { describe('props', () => { test('button', () => { const component = shallow( - + ); expect(component.find(NoDataCard).props().button).toBe('Button'); expect(component).toMatchSnapshot(); @@ -55,12 +34,7 @@ describe('ElasticAgentCardComponent', () => { test('href', () => { const component = shallow( - + ); expect(component.find(NoDataCard).props().href).toBe('some path'); expect(component).toMatchSnapshot(); diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.tsx index 0bca3929f4c2d..7b046bbe3fe8c 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.component.tsx @@ -9,16 +9,12 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTextColor } from '@elastic/eui'; -import { Observable } from 'rxjs'; import { ElasticAgentCardProps } from './types'; import { NoDataCard } from './no_data_card'; import ElasticAgentCardIllustration from './assets/elastic_agent_card.svg'; -import { RedirectAppLinks } from '../../../redirect_app_links'; export type ElasticAgentCardComponentProps = ElasticAgentCardProps & { canAccessFleet: boolean; - navigateToUrl: (url: string) => Promise; - currentAppId$: Observable; }; const noPermissionTitle = i18n.translate( @@ -54,32 +50,19 @@ const elasticAgentCardDescription = i18n.translate( */ export const ElasticAgentCardComponent: FunctionComponent = ({ canAccessFleet, - title, - navigateToUrl, - currentAppId$, + title = elasticAgentCardTitle, ...cardRest }) => { - const noAccessCard = ( - {noPermissionTitle}} - description={{noPermissionDescription}} - isDisabled - {...cardRest} - /> - ); - const card = ( - - ); + const props = canAccessFleet + ? { + title, + description: elasticAgentCardDescription, + } + : { + title: {noPermissionTitle}, + description: {noPermissionDescription}, + isDisabled: true, + }; - return ( - - {canAccessFleet ? card : noAccessCard} - - ); + return ; }; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx index 77c41cddde6da..84cbfb1c73a94 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.stories.tsx @@ -7,29 +7,23 @@ */ import React from 'react'; -import { applicationServiceFactory } from '@kbn/shared-ux-storybook'; import { - ElasticAgentCardComponent, - ElasticAgentCardComponentProps, + ElasticAgentCardComponent as Component, + ElasticAgentCardComponentProps as ComponentProps, } from './elastic_agent_card.component'; +import { ElasticAgentCard } from './elastic_agent_card'; + export default { title: 'Page Template/No Data/Elastic Agent Data Card', description: 'A solution-specific wrapper around NoDataCard, to be used on NoData page', }; -type Params = Pick; +type Params = Pick; export const PureComponent = (params: Params) => { - const { currentAppId$, navigateToUrl } = applicationServiceFactory(); - return ( - - ); + return ; }; PureComponent.argTypes = { @@ -38,3 +32,7 @@ PureComponent.argTypes = { defaultValue: true, }, }; + +export const ConnectedComponent = () => { + return ; +}; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index 42d42dd805650..3702dd4a456a7 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -6,8 +6,10 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { useApplication, useHttp, usePermissions } from '@kbn/shared-ux-services'; +import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import useObservable from 'react-use/lib/useObservable'; import { ElasticAgentCardProps } from './types'; import { ElasticAgentCardComponent } from './elastic_agent_card.component'; @@ -16,27 +18,28 @@ export const ElasticAgentCard = (props: ElasticAgentCardProps) => { const { canAccessFleet } = usePermissions(); const { addBasePath } = useHttp(); const { navigateToUrl, currentAppId$ } = useApplication(); + const currentAppId = useObservable(currentAppId$); - const createHref = () => { - const { href, category } = props; - if (href) { - return href; + const { href: srcHref, category } = props; + + const href = useMemo(() => { + if (srcHref) { + return srcHref; } + // TODO: get this URL from a locator const prefix = '/app/integrations/browse'; + if (category) { return addBasePath(`${prefix}/${category}`); } + return addBasePath(prefix); - }; + }, [addBasePath, srcHref, category]); return ( - + + + ); }; diff --git a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx index f16f87039a626..837eb5282507f 100644 --- a/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx +++ b/packages/kbn-shared-ux-components/src/page_template/no_data_page/no_data_page.tsx @@ -7,14 +7,15 @@ */ import React, { useMemo, FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; import { EuiLink, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import classNames from 'classnames'; +import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; + import { ElasticAgentCard } from './no_data_card'; import { NoDataPageProps } from './types'; -import { KibanaSolutionAvatar } from '../../solution_avatar'; export const NoDataPage: FunctionComponent = ({ solution, diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap b/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap index fce0e996d99cd..069192708e47b 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap @@ -178,7 +178,7 @@ exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = ` className="kbnPageTemplateSolutionNav" heading={ - - & { diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/click_handler.ts b/packages/kbn-shared-ux-components/src/redirect_app_links/click_handler.ts deleted file mode 100644 index db2990726dc93..0000000000000 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/click_handler.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { getClosestLink, hasActiveModifierKey } from '@kbn/shared-ux-utility'; - -interface CreateCrossAppClickHandlerOptions { - navigateToUrl(url: string): Promise; - container?: HTMLElement; -} - -export const createNavigateToUrlClickHandler = ({ - container, - navigateToUrl, -}: CreateCrossAppClickHandlerOptions): React.MouseEventHandler => { - return (e) => { - if (!container) { - return; - } - // see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239 - const target = e.target as HTMLElement; - - const link = getClosestLink(target, container); - if (!link) { - return; - } - - const isNotEmptyHref = link.href; - const hasNoTarget = link.target === '' || link.target === '_self'; - const isLeftClickOnly = e.button === 0; - - if ( - isNotEmptyHref && - hasNoTarget && - isLeftClickOnly && - !e.defaultPrevented && - !hasActiveModifierKey(e) - ) { - e.preventDefault(); - navigateToUrl(link.href); - } - }; -}; diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/index.ts b/packages/kbn-shared-ux-components/src/redirect_app_links/index.ts deleted file mode 100644 index db7462d7cb1bf..0000000000000 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/index.ts +++ /dev/null @@ -1,18 +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. - */ -/* eslint-disable import/no-default-export */ - -import { RedirectAppLinks } from './redirect_app_links'; -export type { RedirectAppLinksProps } from './redirect_app_links'; -export { RedirectAppLinks } from './redirect_app_links'; - -/** - * Exporting the RedirectAppLinks component as a default export so it can be - * loaded by React.lazy. - */ -export default RedirectAppLinks; diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.mdx b/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.mdx deleted file mode 100644 index 0023182940ae9..0000000000000 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -id: sharedUX/Components/AppLink -slug: /shared-ux/components/redirect-app-link -title: Redirect App Link -summary: The component for redirect links. -tags: ['shared-ux', 'component'] -date: 2022-02-01 ---- - -> This documentation is in progress. - -**This component has been refactored.** Instead of requiring the entire `application`, it instead takes just `navigateToUrl` and `currentAppId$`. This makes the component more lightweight. diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.stories.tsx b/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.stories.tsx deleted file mode 100644 index 0ca0e2a8d9978..0000000000000 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.stories.tsx +++ /dev/null @@ -1,43 +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 { EuiButton } from '@elastic/eui'; -import React from 'react'; -import { BehaviorSubject } from 'rxjs'; - -import { action } from '@storybook/addon-actions'; -import { RedirectAppLinks } from './redirect_app_links'; -import mdx from './redirect_app_links.mdx'; - -export default { - title: 'Redirect App Links', - description: 'app links component that takes in an application id and navigation url.', - parameters: { - docs: { - page: mdx, - }, - }, -}; - -export const Component = () => { - return ( - Promise.resolve()} - currentAppId$={new BehaviorSubject('test')} - > - - Test link - - - ); -}; diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.test.tsx b/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.test.tsx deleted file mode 100644 index d36bace70b7c8..0000000000000 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.test.tsx +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { MouseEvent } from 'react'; -import { mount } from 'enzyme'; -import { BehaviorSubject } from 'rxjs'; - -import { RedirectAppLinks } from './redirect_app_links'; - -export type UnmountCallback = () => void; -export type MountPoint = (element: T) => UnmountCallback; - -const createServiceMock = () => { - const currentAppId$ = new BehaviorSubject('currentApp'); - - return { - currentAppId$: currentAppId$.asObservable(), - navigateToApp: jest.fn(), - navigateToUrl: jest.fn(), - }; -}; - -/* eslint-disable jsx-a11y/click-events-have-key-events */ - -describe('RedirectAppLinks', () => { - let application = createServiceMock(); - - beforeEach(() => { - application = createServiceMock(); - }); - - it('intercept click events on children link elements', () => { - let event: MouseEvent; - const component = mount( -
{ - event = e; - }} - > - -
- content -
-
-
- ); - - component.find('a').simulate('click', { button: 0, defaultPrevented: false }); - expect(application.navigateToUrl).toHaveBeenCalledTimes(1); - expect(event!.defaultPrevented).toBe(true); - }); - - it('intercept click events on children inside link elements', async () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - - - -
- ); - - component.find('span').simulate('click', { button: 0, defaultPrevented: false }); - - expect(application.navigateToUrl).toHaveBeenCalledTimes(1); - expect(event!.defaultPrevented).toBe(true); - }); - - it('does not intercept click events when the target is not inside a link', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - - - content - - -
- ); - - component.find('span').simulate('click', { button: 0, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!.defaultPrevented).toBe(false); - }); - - it('does not intercept click events when the link is a parent of the container', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - - - content - - -
- ); - - component.find('span').simulate('click', { button: 0, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!.defaultPrevented).toBe(false); - }); - - it('does not intercept click events when the link has an external target', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - - - content - - -
- ); - - component.find('a').simulate('click', { button: 0, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!.defaultPrevented).toBe(false); - }); - - it('does not intercept click events when the event is already defaultPrevented', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - - - e.preventDefault()}>content - - -
- ); - - component.find('span').simulate('click', { button: 0, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!.defaultPrevented).toBe(true); - }); - - it('does not intercept click events when the event propagation is stopped', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - - e.stopPropagation()}> - content - - -
- ); - - component.find('a').simulate('click', { button: 0, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!).toBe(undefined); - }); - - it('does not intercept click events when the event is not triggered from the left button', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - -
- content -
-
-
- ); - - component.find('a').simulate('click', { button: 1, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!.defaultPrevented).toBe(false); - }); - - it('does not intercept click events when the event has a modifier key enabled', () => { - let event: MouseEvent; - - const component = mount( -
{ - event = e; - }} - > - -
- content -
-
-
- ); - - component.find('a').simulate('click', { button: 0, ctrlKey: true, defaultPrevented: false }); - - expect(application.navigateToApp).not.toHaveBeenCalled(); - expect(event!.defaultPrevented).toBe(false); - }); -}); diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.tsx b/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.tsx deleted file mode 100644 index e1d0bd4bed653..0000000000000 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/redirect_app_links.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useRef, useMemo } from 'react'; -import type { HTMLAttributes, DetailedHTMLProps, FC } from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { Observable } from 'rxjs'; - -import { createNavigateToUrlClickHandler } from './click_handler'; - -type DivProps = DetailedHTMLProps, HTMLDivElement>; -/** - * TODO: this interface recreates props from the `ApplicationStart` interface. - * see: https://github.com/elastic/kibana/issues/127695 - */ -export interface RedirectAppLinksProps extends DivProps { - currentAppId$: Observable; - navigateToUrl(url: string): Promise; -} - -/** - * Utility component that will intercept click events on children anchor (``) elements to call - * `application.navigateToUrl` with the link's href. This will trigger SPA friendly navigation - * when the link points to a valid Kibana app. - * - * @example - * ```tsx - * url} currentAppId$={observableAppId}> - * Go to another-app - * - * ``` - * - * @remarks - * It is recommended to use the component at the highest possible level of the component tree that would - * require to handle the links. A good practice is to consider it as a context provider and to use it - * at the root level of an application or of the page that require the feature. - */ -export const RedirectAppLinks: FC = ({ - navigateToUrl, - currentAppId$, - children, - ...otherProps -}) => { - const currentAppId = useObservable(currentAppId$, undefined); - const containerRef = useRef(null); - const clickHandler = useMemo( - () => - containerRef.current && currentAppId - ? createNavigateToUrlClickHandler({ - container: containerRef.current, - navigateToUrl, - }) - : undefined, - [currentAppId, navigateToUrl] - ); - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events -
- {children} -
- ); -}; diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.stories.tsx b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.stories.tsx deleted file mode 100644 index bc26806016df0..0000000000000 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { KibanaSolutionAvatar, KibanaSolutionAvatarProps } from './solution_avatar'; - -export default { - title: 'Solution Avatar', - description: 'A wrapper around EuiAvatar, specifically to stylize Elastic Solutions', -}; - -type Params = Pick; - -export const PureComponent = (params: Params) => { - return ; -}; - -PureComponent.argTypes = { - name: { - control: 'text', - defaultValue: 'Kibana', - }, - size: { - control: 'radio', - options: ['s', 'm', 'l', 'xl', 'xxl'], - defaultValue: 'xxl', - }, -}; diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.tsx b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.tsx deleted file mode 100644 index deb71affc9c1a..0000000000000 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.tsx +++ /dev/null @@ -1,44 +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 './solution_avatar.scss'; - -import React from 'react'; - -import { DistributiveOmit, EuiAvatar, EuiAvatarProps } from '@elastic/eui'; -import classNames from 'classnames'; - -export type KibanaSolutionAvatarProps = DistributiveOmit & { - /** - * Any EuiAvatar size available, or `xxl` for custom large, brand-focused version - */ - size?: EuiAvatarProps['size'] | 'xxl'; -}; - -/** - * Applies extra styling to a typical EuiAvatar. - * The `name` value will be appended to 'logo' to configure the `iconType` unless `iconType` is provided. - */ -export const KibanaSolutionAvatar = ({ className, size, ...rest }: KibanaSolutionAvatarProps) => { - return ( - // @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine - - ); -}; diff --git a/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap b/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap index c3b7dc63bce94..8091bd222d1a3 100644 --- a/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap +++ b/packages/kbn-shared-ux-components/src/toolbar/buttons/icon_button_group/__snapshots__/icon_button_group.test.tsx.snap @@ -4,7 +4,9 @@ exports[` is rendered 1`] = ` is rendered 1`] = ` ({ navigateToUrl: () => Promise.resolve(), - currentAppId$: new Observable(), + currentAppId$: new Observable((subscriber) => { + subscriber.next('abc123'); + }), }); diff --git a/packages/kbn-shared-ux-storybook/src/services/application.ts b/packages/kbn-shared-ux-storybook/src/services/application.ts index 2a544445fc474..1b16526bc8be8 100644 --- a/packages/kbn-shared-ux-storybook/src/services/application.ts +++ b/packages/kbn-shared-ux-storybook/src/services/application.ts @@ -16,8 +16,8 @@ export type ApplicationServiceFactory = ServiceFactory ({ - navigateToUrl: () => { - action('NavigateToUrl'); + navigateToUrl: (url) => { + action('navigateToUrl')(url); return Promise.resolve(); }, currentAppId$: new BehaviorSubject('123'), diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 42dc19445c293..c065cb01a4c36 100644 --- a/packages/kbn-test/src/es/test_es_cluster.ts +++ b/packages/kbn-test/src/es/test_es_cluster.ts @@ -146,6 +146,11 @@ export interface CreateTestEsClusterOptions { * defaults to the transport port from `packages/kbn-test/src/es/es_test_config.ts` */ transportPort?: number | string; + /** + * Report to the creator of the es-test-cluster that the es node has exitted before stop() was called, allowing + * this caller to react appropriately. If this is not passed then an uncatchable exception will be thrown + */ + onEarlyExit?: (msg: string) => void; } export function createTestEsCluster< @@ -165,6 +170,7 @@ export function createTestEsCluster< clusterName: customClusterName = 'es-test-cluster', ssl, transportPort, + onEarlyExit, } = options; const clusterName = `${CI_PARALLEL_PROCESS_PREFIX}${customClusterName}`; @@ -258,6 +264,7 @@ export function createTestEsCluster< // set it up after the last node is started. skipNativeRealmSetup: this.nodes.length > 1 && i < this.nodes.length - 1, skipReadyCheck: this.nodes.length > 1 && i < this.nodes.length - 1, + onEarlyExit, }); }); } diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 4159533e628bc..f71e4ac7d6ccd 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { resolve } from 'path'; +import Path from 'path'; import { inspect } from 'util'; import { run, createFlagError, Flags } from '@kbn/dev-utils'; @@ -16,7 +16,7 @@ import exitHook from 'exit-hook'; import { FunctionalTestRunner } from './functional_test_runner'; -const makeAbsolutePath = (v: string) => resolve(process.cwd(), v); +const makeAbsolutePath = (v: string) => Path.resolve(process.cwd(), v); const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); const parseInstallDir = (flags: Flags) => { const flag = flags['kibana-install-dir']; @@ -42,9 +42,15 @@ export function runFtrCli() { throw createFlagError('expected --es-version to be a string'); } + const configRel = flags.config; + if (typeof configRel !== 'string' || !configRel) { + throw createFlagError('--config is required'); + } + const configPath = makeAbsolutePath(configRel); + const functionalTestRunner = new FunctionalTestRunner( log, - makeAbsolutePath(flags.config as string), + configPath, { mochaOpts: { bail: flags.bail, @@ -69,6 +75,8 @@ export function runFtrCli() { esVersion ); + await functionalTestRunner.readConfigFile(); + if (flags.throttle) { process.env.TEST_THROTTLE_NETWORK = '1'; } @@ -149,9 +157,6 @@ export function runFtrCli() { 'headless', 'dry-run', ], - default: { - config: 'test/functional/config.js', - }, help: ` --config=path path to a config file --bail stop tests after the first failure diff --git a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts index 96ebcd79c4e43..506b6f139f736 100644 --- a/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts +++ b/packages/kbn-test/src/functional_test_runner/fake_mocha_types.ts @@ -37,6 +37,7 @@ export interface Test { export interface Runner extends EventEmitter { abort(): void; failures: any[]; + uncaught: (error: Error) => void; } export interface Mocha { diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 0ceba511f9b9b..9de6500a45323 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -43,7 +43,7 @@ export class FunctionalTestRunner { : new EsVersion(esVersion); } - async run() { + async run(abortSignal?: AbortSignal) { const testStats = await this.getTestStats(); return await this.runHarness(async (config, lifecycle, coreProviders) => { @@ -106,10 +106,19 @@ export class FunctionalTestRunner { return this.simulateMochaDryRun(mocha); } + if (abortSignal?.aborted) { + this.log.warning('run aborted'); + return; + } + await lifecycle.beforeTests.trigger(mocha.suite); - this.log.info('Starting tests'); + if (abortSignal?.aborted) { + this.log.warning('run aborted'); + return; + } - return await runTests(lifecycle, mocha); + this.log.info('Starting tests'); + return await runTests(lifecycle, mocha, abortSignal); }); } @@ -210,12 +219,7 @@ export class FunctionalTestRunner { const lifecycle = new Lifecycle(this.log); try { - const config = await readConfigFile( - this.log, - this.esVersion, - this.configFile, - this.configOverrides - ); + const config = await this.readConfigFile(); this.log.debug('Config loaded'); if ( @@ -259,6 +263,10 @@ export class FunctionalTestRunner { } } + public async readConfigFile() { + return await readConfigFile(this.log, this.esVersion, this.configFile, this.configOverrides); + } + simulateMochaDryRun(mocha: any) { interface TestEntry { file: string; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 24702d699064c..49a6ef16d6685 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -10,6 +10,7 @@ import Path from 'path'; import { ToolingLog } from '@kbn/tooling-log'; import { defaultsDeep } from 'lodash'; import { createFlagError } from '@kbn/dev-utils'; +import { REPO_ROOT } from '@kbn/utils'; import { Config } from './config'; import { EsVersion } from '../es_version'; @@ -26,21 +27,33 @@ async function getSettingsFromFile( primary: boolean; } ) { + let resolvedPath; + try { + resolvedPath = require.resolve(options.path); + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + throw createFlagError(`Unable to find config file [${options.path}]`); + } + + throw error; + } + if ( options.primary && - !FTR_CONFIGS_MANIFEST_PATHS.includes(options.path) && - !options.path.includes(`${Path.sep}__fixtures__${Path.sep}`) + !FTR_CONFIGS_MANIFEST_PATHS.includes(resolvedPath) && + !resolvedPath.includes(`${Path.sep}__fixtures__${Path.sep}`) ) { + const rel = Path.relative(REPO_ROOT, resolvedPath); throw createFlagError( - `Refusing to load FTR Config which is not listed in [${FTR_CONFIGS_MANIFEST_REL}]. All FTR Config files must be listed there, use the "enabled" key if the FTR Config should be run on automatically on PR CI, or the "disabled" key if it is run manually or by a special job.` + `Refusing to load FTR Config at [${rel}] which is not listed in [${FTR_CONFIGS_MANIFEST_REL}]. All FTR Config files must be listed there, use the "enabled" key if the FTR Config should be run on automatically on PR CI, or the "disabled" key if it is run manually or by a special job.` ); } - const configModule = require(options.path); // eslint-disable-line @typescript-eslint/no-var-requires + const configModule = require(resolvedPath); // eslint-disable-line @typescript-eslint/no-var-requires const configProvider = configModule.__esModule ? configModule.default : configModule; if (!cache.has(configProvider)) { - log.debug('Loading config file from %j', options.path); + log.debug('Loading config file from %j', resolvedPath); cache.set( configProvider, configProvider({ diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts index 89f0ea088cac8..12840b77dd8d9 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/run_tests.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import * as Rx from 'rxjs'; import { Lifecycle } from '../lifecycle'; import { Mocha } from '../../fake_mocha_types'; @@ -18,14 +19,23 @@ import { Mocha } from '../../fake_mocha_types'; * @param {Mocha} mocha * @return {Promise} resolves to the number of test failures */ -export async function runTests(lifecycle: Lifecycle, mocha: Mocha) { +export async function runTests(lifecycle: Lifecycle, mocha: Mocha, abortSignal?: AbortSignal) { let runComplete = false; const runner = mocha.run(() => { runComplete = true; }); - lifecycle.cleanup.add(() => { - if (!runComplete) runner.abort(); + Rx.race( + lifecycle.cleanup.before$, + abortSignal ? Rx.fromEvent(abortSignal, 'abort').pipe(Rx.take(1)) : Rx.NEVER + ).subscribe({ + next() { + if (!runComplete) { + runComplete = true; + runner.uncaught(new Error('Forcing mocha to abort')); + runner.abort(); + } + }, }); return new Promise((resolve) => { diff --git a/packages/kbn-test/src/functional_tests/lib/index.ts b/packages/kbn-test/src/functional_tests/lib/index.ts index bf2cc43159526..2726192328bda 100644 --- a/packages/kbn-test/src/functional_tests/lib/index.ts +++ b/packages/kbn-test/src/functional_tests/lib/index.ts @@ -10,5 +10,5 @@ export { runKibanaServer } from './run_kibana_server'; export { runElasticsearch } from './run_elasticsearch'; export type { CreateFtrOptions, CreateFtrParams } from './run_ftr'; export { runFtr, hasTests, assertNoneExcluded } from './run_ftr'; -export { KIBANA_ROOT, KIBANA_FTR_SCRIPT, FUNCTIONAL_CONFIG_PATH, API_CONFIG_PATH } from './paths'; +export { KIBANA_ROOT, KIBANA_FTR_SCRIPT } from './paths'; export { runCli } from './run_cli'; diff --git a/packages/kbn-test/src/functional_tests/lib/paths.ts b/packages/kbn-test/src/functional_tests/lib/paths.ts index 37cd708de1e00..75a654fdfc513 100644 --- a/packages/kbn-test/src/functional_tests/lib/paths.ts +++ b/packages/kbn-test/src/functional_tests/lib/paths.ts @@ -19,6 +19,3 @@ export const KIBANA_EXEC = 'node'; export const KIBANA_EXEC_PATH = resolveRelative('scripts/kibana'); export const KIBANA_ROOT = REPO_ROOT; export const KIBANA_FTR_SCRIPT = resolve(KIBANA_ROOT, 'scripts/functional_test_runner'); -export const PROJECT_ROOT = resolve(__dirname, '../../../../../../'); -export const FUNCTIONAL_CONFIG_PATH = resolve(KIBANA_ROOT, 'test/functional/config'); -export const API_CONFIG_PATH = resolve(KIBANA_ROOT, 'test/api_integration/config'); diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index adbb18b5312d0..2ee9de4053fef 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -17,6 +17,7 @@ interface RunElasticsearchOptions { log: ToolingLog; esFrom?: string; config: Config; + onEarlyExit?: (msg: string) => void; } interface CcsConfig { @@ -92,7 +93,8 @@ export async function runElasticsearch( async function startEsNode( log: ToolingLog, name: string, - config: EsConfig & { transportPort?: number } + config: EsConfig & { transportPort?: number }, + onEarlyExit?: (msg: string) => void ) { const cluster = createTestEsCluster({ clusterName: `cluster-${name}`, @@ -112,6 +114,7 @@ async function startEsNode( }, ], transportPort: config.transportPort, + onEarlyExit, }); await cluster.start(); diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts index 4c4a7128a05a9..b9945adbdfb56 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts @@ -81,8 +81,8 @@ async function createFtr({ }; } -export async function assertNoneExcluded({ configPath, options }: CreateFtrParams) { - const { config, ftr } = await createFtr({ configPath, options }); +export async function assertNoneExcluded(params: CreateFtrParams) { + const { config, ftr } = await createFtr(params); if (config.get('testRunner')) { // tests with custom test runners are not included in this check @@ -95,21 +95,21 @@ export async function assertNoneExcluded({ configPath, options }: CreateFtrParam } if (stats.testsExcludedByTag.length > 0) { throw new CliError(` - ${stats.testsExcludedByTag.length} tests in the ${configPath} config + ${stats.testsExcludedByTag.length} tests in the ${params.configPath} config are excluded when filtering by the tags run on CI. Make sure that all suites are tagged with one of the following tags: - ${JSON.stringify(options.suiteTags)} + ${JSON.stringify(params.options.suiteTags)} - ${stats.testsExcludedByTag.join('\n - ')} `); } } -export async function runFtr({ configPath, options }: CreateFtrParams) { - const { ftr } = await createFtr({ configPath, options }); +export async function runFtr(params: CreateFtrParams, signal?: AbortSignal) { + const { ftr } = await createFtr(params); - const failureCount = await ftr.run(); + const failureCount = await ftr.run(signal); if (failureCount > 0) { throw new CliError( `${failureCount} functional test ${failureCount === 1 ? 'failure' : 'failures'}` @@ -117,8 +117,8 @@ export async function runFtr({ configPath, options }: CreateFtrParams) { } } -export async function hasTests({ configPath, options }: CreateFtrParams) { - const { ftr, config } = await createFtr({ configPath, options }); +export async function hasTests(params: CreateFtrParams) { + const { ftr, config } = await createFtr(params); if (config.get('testRunner')) { // configs with custom test runners are assumed to always have tests diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts index 47d0b1c93b620..b5026d397139d 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.ts @@ -31,10 +31,12 @@ export async function runKibanaServer({ procs, config, options, + onEarlyExit, }: { procs: ProcRunner; config: Config; options: { installDir?: string; extraKbnOpts?: string[] }; + onEarlyExit?: (msg: string) => void; }) { const runOptions = config.get('kbnTestServer.runOptions'); const installDir = runOptions.alwaysUseSource ? undefined : options.installDir; @@ -51,6 +53,7 @@ export async function runKibanaServer({ }, cwd: installDir || KIBANA_ROOT, wait: runOptions.wait, + onEarlyExit, }); } diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index dd9fe4c93016c..33a49ae2c80d1 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -107,14 +107,26 @@ export async function runTests(options: RunTestsParams) { await withProcRunner(log, async (procs) => { const config = await readConfigFile(log, options.esVersion, configPath); + const abortCtrl = new AbortController(); + + const onEarlyExit = (msg: string) => { + log.error(msg); + abortCtrl.abort(); + }; let shutdownEs; try { if (process.env.TEST_ES_DISABLE_STARTUP !== 'true') { - shutdownEs = await runElasticsearch({ ...options, log, config }); + shutdownEs = await runElasticsearch({ ...options, log, config, onEarlyExit }); + if (abortCtrl.signal.aborted) { + return; + } + } + await runKibanaServer({ procs, config, options, onEarlyExit }); + if (abortCtrl.signal.aborted) { + return; } - await runKibanaServer({ procs, config, options }); - await runFtr({ configPath, options: { ...options, log } }); + await runFtr({ configPath, options: { ...options, log } }, abortCtrl.signal); } finally { try { const delay = config.get('kbnTestServer.delayShutdown'); diff --git a/packages/shared-ux/avatar/solution/BUILD.bazel b/packages/shared-ux/avatar/solution/BUILD.bazel new file mode 100644 index 0000000000000..a253153cb9227 --- /dev/null +++ b/packages/shared-ux/avatar/solution/BUILD.bazel @@ -0,0 +1,146 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "solution" +PKG_REQUIRE_NAME = "@kbn/shared-ux-avatar-solution" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.scss", + "src/**/*.mdx", + "src/**/*.svg", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//@elastic/eui", + "@npm//classnames", + "@npm//enzyme", + "@npm//react", + "@npm//url-loader", + "//packages/kbn-i18n-react", + "//packages/kbn-i18n", + "//packages/kbn-shared-ux-utility", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@elastic/eui", + "@npm//@storybook/addon-actions", + "@npm//@types/classnames", + "@npm//@types/enzyme", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", + "//packages/kbn-ambient-ui-types", + "//packages/kbn-i18n-react:npm_module_types", + "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-shared-ux-utility:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/avatar/solution/README.mdx b/packages/shared-ux/avatar/solution/README.mdx new file mode 100644 index 0000000000000..841274441f6ed --- /dev/null +++ b/packages/shared-ux/avatar/solution/README.mdx @@ -0,0 +1,26 @@ +--- +id: sharedUX/Components/KibanaSolutionAvatar +slug: /shared-ux/components/avatar-solution +title: Solution Avatar +summary: A wrapper around `EuiAvatar` tailored for use in Kibana solutions. +tags: ['shared-ux', 'component'] +date: 2022-05-04 +--- + +## Description + +A wrapper around `EuiAvatar` tailored for use in Kibana solutions. + +## Usage + +If using for a known solution, (e.g. one whose logo is in EUI as `logoSomeSolution`), you can simply set the `name` prop: + +```tsx + +``` + +If the name provided does not match a known solution, you *must* set the `iconType` prop: + +```tsx + +``` diff --git a/packages/shared-ux/avatar/solution/jest.config.js b/packages/shared-ux/avatar/solution/jest.config.js new file mode 100644 index 0000000000000..6ca49f67e1dd5 --- /dev/null +++ b/packages/shared-ux/avatar/solution/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/avatar/solution'], +}; diff --git a/packages/shared-ux/avatar/solution/package.json b/packages/shared-ux/avatar/solution/package.json new file mode 100644 index 0000000000000..b0ec8ec947b09 --- /dev/null +++ b/packages/shared-ux/avatar/solution/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/shared-ux-avatar-solution", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/__snapshots__/solution_avatar.test.tsx.snap b/packages/shared-ux/avatar/solution/src/__snapshots__/solution_avatar.test.tsx.snap similarity index 54% rename from packages/kbn-shared-ux-components/src/solution_avatar/__snapshots__/solution_avatar.test.tsx.snap rename to packages/shared-ux/avatar/solution/src/__snapshots__/solution_avatar.test.tsx.snap index 9817d7cdd8d45..f0666987e0f79 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/__snapshots__/solution_avatar.test.tsx.snap +++ b/packages/shared-ux/avatar/solution/src/__snapshots__/solution_avatar.test.tsx.snap @@ -8,3 +8,12 @@ exports[`KibanaSolutionAvatar renders 1`] = ` name="Solution" /> `; + +exports[`KibanaSolutionAvatar renders 2`] = ` + +`; diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/assets/texture.svg b/packages/shared-ux/avatar/solution/src/assets/texture.svg similarity index 100% rename from packages/kbn-shared-ux-components/src/solution_avatar/assets/texture.svg rename to packages/shared-ux/avatar/solution/src/assets/texture.svg diff --git a/packages/shared-ux/avatar/solution/src/index.tsx b/packages/shared-ux/avatar/solution/src/index.tsx new file mode 100644 index 0000000000000..c2c9613bab87d --- /dev/null +++ b/packages/shared-ux/avatar/solution/src/index.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +export type { KibanaSolutionAvatarProps } from './solution_avatar'; + +/** + * The Lazily-loaded `KibanaSolutionAvatar` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const KibanaSolutionAvatarLazy = React.lazy(() => + import('./solution_avatar').then(({ KibanaSolutionAvatar }) => ({ + default: KibanaSolutionAvatar, + })) +); + +/** + * A `KibanaSolutionAvatar` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `KibanaPageTemplateSolutionNavAvatarLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const KibanaSolutionAvatar = withSuspense(KibanaSolutionAvatarLazy); diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss b/packages/shared-ux/avatar/solution/src/solution_avatar.scss similarity index 100% rename from packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss rename to packages/shared-ux/avatar/solution/src/solution_avatar.scss diff --git a/packages/shared-ux/avatar/solution/src/solution_avatar.stories.tsx b/packages/shared-ux/avatar/solution/src/solution_avatar.stories.tsx new file mode 100644 index 0000000000000..b47ff7c837f24 --- /dev/null +++ b/packages/shared-ux/avatar/solution/src/solution_avatar.stories.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { KibanaSolutionAvatar, IconTypeProps, KnownSolutionProps } from './solution_avatar'; + +export default { + title: 'Solution Avatar', + description: 'A wrapper around EuiAvatar, specifically to stylize Elastic Solutions', +}; + +const argTypes = { + size: { + control: 'select', + options: ['s', 'm', 'l', 'xl', 'xxl'], + defaultValue: 'xxl', + }, +}; + +type KnownSolutionParams = Pick; + +export const SolutionAvatar = (params: KnownSolutionParams) => { + return ; +}; + +SolutionAvatar.argTypes = { + name: { + control: 'select', + options: ['Cloud', 'Elastic', 'Kibana', 'Observability', 'Security', 'Enterprise Search'], + defaultValue: 'Elastic', + }, + ...argTypes, +}; + +type IconTypeParams = Pick; + +export const IconTypeAvatar = (params: IconTypeParams) => { + return ; +}; + +IconTypeAvatar.argTypes = { + iconType: { + control: 'select', + options: [ + 'logoCloud', + 'logoElastic', + 'logoElasticsearch', + 'logoElasticStack', + 'logoKibana', + 'logoObservability', + 'logoSecurity', + 'logoSiteSearch', + 'logoWorkplaceSearch', + 'machineLearningApp', + 'managementApp', + ], + defaultValue: 'logoElastic', + }, + name: { + control: 'text', + defaultValue: 'Solution Name', + }, + ...argTypes, +}; diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.test.tsx b/packages/shared-ux/avatar/solution/src/solution_avatar.test.tsx similarity index 68% rename from packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.test.tsx rename to packages/shared-ux/avatar/solution/src/solution_avatar.test.tsx index 7a8b20c3f8d64..ab7c675b24e0d 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.test.tsx +++ b/packages/shared-ux/avatar/solution/src/solution_avatar.test.tsx @@ -12,7 +12,9 @@ import { KibanaSolutionAvatar } from './solution_avatar'; describe('KibanaSolutionAvatar', () => { test('renders', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); + const nameAndIcon = shallow(); + expect(nameAndIcon).toMatchSnapshot(); + const nameOnly = shallow(); + expect(nameOnly).toMatchSnapshot(); }); }); diff --git a/packages/shared-ux/avatar/solution/src/solution_avatar.tsx b/packages/shared-ux/avatar/solution/src/solution_avatar.tsx new file mode 100644 index 0000000000000..0c38652a27395 --- /dev/null +++ b/packages/shared-ux/avatar/solution/src/solution_avatar.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 './solution_avatar.scss'; + +import React from 'react'; +import classNames from 'classnames'; + +import { DistributiveOmit, EuiAvatar, EuiAvatarProps, IconType } from '@elastic/eui'; + +import { SolutionNameType } from './types'; + +export type KnownSolutionProps = DistributiveOmit & { + /** + * Any EuiAvatar size available, or `xxl` for custom large, brand-focused version + */ + size?: EuiAvatarProps['size'] | 'xxl'; + name: SolutionNameType; +}; + +export type IconTypeProps = DistributiveOmit & { + /** + * Any EuiAvatar size available, or `xxl` for custom large, brand-focused version + */ + size?: EuiAvatarProps['size'] | 'xxl'; + name?: string; + iconType: IconType; +}; + +const isKnown = (props: any): props is KnownSolutionProps => { + return typeof props.iconType === 'undefined'; +}; + +export type KibanaSolutionAvatarProps = KnownSolutionProps | IconTypeProps; + +/** + * Applies extra styling to a typical EuiAvatar. + * The `name` value will be appended to 'logo' to configure the `iconType` unless `iconType` is provided. + */ +export const KibanaSolutionAvatar = (props: KibanaSolutionAvatarProps) => { + const { className, size, ...rest } = props; + + // If the name is a known solution, use the name to set the correct IconType. + // Create an empty object so `iconType` remains undefined or inherited from `props`. + const icon: { + iconType?: IconType; + } = {}; + + if (isKnown(props)) { + icon.iconType = `logo${props.name.replace(/\s+/g, '')}`; + } + + return ( + // @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine + + ); +}; diff --git a/packages/shared-ux/avatar/solution/src/types.ts b/packages/shared-ux/avatar/solution/src/types.ts new file mode 100644 index 0000000000000..bf0ad682e3006 --- /dev/null +++ b/packages/shared-ux/avatar/solution/src/types.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +// Manual, exhaustive list at present. This was attempted dynamically using Typescript Template Literals and +// the computation cost exceeded the benefit. By enumerating them manually, we reduce the complexity of TS +// checking at the expense of not being dynamic against a very, very static list. +// +// The only consequence is requiring a solution name without a space, (e.g. `ElasticStack`) until it's added +// here. That's easy to do in the very unlikely event that ever happens. +export type SolutionNameType = + | 'App Search' + | 'Beats' + | 'Business Analytics' + | 'Cloud' + | 'Cloud Enterprise' + | 'Code' + | 'Elastic' + | 'Elastic Stack' + | 'Elasticsearch' + | 'Enterprise Search' + | 'Logstash' + | 'Maps' + | 'Metrics' + | 'Observability' + | 'Security' + | 'Site Search' + | 'Uptime' + | 'Webhook' + | 'Workplace Search'; diff --git a/packages/shared-ux/avatar/solution/tsconfig.json b/packages/shared-ux/avatar/solution/tsconfig.json new file mode 100644 index 0000000000000..93076efae5d7c --- /dev/null +++ b/packages/shared-ux/avatar/solution/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/shared-ux/link/redirect_app/BUILD.bazel b/packages/shared-ux/link/redirect_app/BUILD.bazel new file mode 100644 index 0000000000000..861b9aa277db9 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/BUILD.bazel @@ -0,0 +1,140 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "redirect_app" +PKG_REQUIRE_NAME = "@kbn/shared-ux-link-redirect-app" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.mdx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//@elastic/eui", + "@npm//@storybook/addon-actions", + "@npm//react-use", + "@npm//react", + "@npm//rxjs", + "//packages/kbn-shared-ux-utility", +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@elastic/eui", + "@npm//@storybook/addon-actions", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/react", + "@npm//rxjs", + "@npm//react-use", + "//packages/kbn-ambient-ui-types", + "//packages/kbn-shared-ux-utility:npm_module_types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/link/redirect_app/README.mdx b/packages/shared-ux/link/redirect_app/README.mdx new file mode 100644 index 0000000000000..8e2eada760ea2 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/README.mdx @@ -0,0 +1,86 @@ +--- +id: sharedUX/Components/AppLink +slug: /shared-ux/components/redirect-app-links +title: Redirect App Links +summary: A component for redirecting links contained within it to the appropriate Kibana solution without a page refresh. +tags: ['shared-ux', 'component'] +date: 2022-05-04 +--- + +## Description + +This component is an "area of effect" component, which produces a container that intercepts actions for specific elements within it. In this case, the container intercepts clicks on anchor elements and redirects them to Kibana solutions without a page refresh. + +## Pure Component + +The pure component allows you create a container to intercept clicks without contextual services, (e.g. Kibana Core). This likely does not have much utility for solutions in Kibana, but rather is useful for shared components where we want to ensure clicks are redirected correctly. + +```tsx +import { RedirectAppLinksComponent as RedirectAppLinks } from '@kbn/shared-ux-links-redirect-app'; + + { ... }}> + Go to another-app + +``` + +## Connected Component + +The connected component uses a React Context to access services that provide the current app id and a function to navigate to a new url. This is useful in that a solution can wrap their entire application in the context and use `RedirectAppLinks` in specific areas. + +```tsx +import { RedirectAppLinksContainer as RedirectAppLinks, RedirectAppLinksProvider } from '@kbn/shared-ux-links-redirect-app'; + + { ... }}> + . + {/* other components that don't need to redirect */} + . + + Go to another-app + + . + . + . + +``` + +You can also use the Kibana provider: + +```tsx +import { + RedirectAppLinksContainer as RedirectAppLinks, + RedirectAppLinksKibanaProvider as RedirectAppLinksProvider +} from '@kbn/shared-ux-links-redirect-app'; + + + . + {/* other components that don't need to redirect */} + . + + Go to another-app + + . + . + +``` + +## Top-level Component + +This is the component is likely the most useful to solutions in Kibana. It assumes an entire solution needs this redirect functionality, and combines the context provider with the container. This top-level component can be used with either pure props or with Kibana services. + +```tsx +import { RedirectAppLinks } from '@kbn/shared-ux-links-redirect-app'; + + { ... }}> + . + Go to another-app + . + + +{/* OR */} + + + . + Go to another-app + . + +``` \ No newline at end of file diff --git a/packages/shared-ux/link/redirect_app/jest.config.js b/packages/shared-ux/link/redirect_app/jest.config.js new file mode 100644 index 0000000000000..5f564a9709d0c --- /dev/null +++ b/packages/shared-ux/link/redirect_app/jest.config.js @@ -0,0 +1,14 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/link/redirect_app'], + verbose: true, +}; diff --git a/packages/shared-ux/link/redirect_app/package.json b/packages/shared-ux/link/redirect_app/package.json new file mode 100644 index 0000000000000..6deb187dcec2a --- /dev/null +++ b/packages/shared-ux/link/redirect_app/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/shared-ux-link-redirect-app", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/kbn-shared-ux-components/src/redirect_app_links/click_handler.test.ts b/packages/shared-ux/link/redirect_app/src/click_handler.test.ts similarity index 84% rename from packages/kbn-shared-ux-components/src/redirect_app_links/click_handler.test.ts rename to packages/shared-ux/link/redirect_app/src/click_handler.test.ts index dd26443eed171..c46b93bb67aaf 100644 --- a/packages/kbn-shared-ux-components/src/redirect_app_links/click_handler.test.ts +++ b/packages/shared-ux/link/redirect_app/src/click_handler.test.ts @@ -7,7 +7,7 @@ */ import { MouseEvent } from 'react'; -import { createNavigateToUrlClickHandler } from './click_handler'; +import { navigateToUrlClickHandler } from './click_handler'; const createLink = ({ href = '/base-path/app/targetApp', @@ -43,27 +43,59 @@ const createEvent = ({ type NavigateToURLFn = (url: string) => Promise; -describe('createNavigateToUrlClickHandler', () => { +describe('navigateToUrlClickHandler', () => { let container: HTMLElement; let navigateToUrl: jest.MockedFunction; + const currentAppId = 'abc123'; - const createHandler = () => - createNavigateToUrlClickHandler({ + const handler = (event: MouseEvent): void => { + navigateToUrlClickHandler({ + event, + currentAppId, container, navigateToUrl, }); + }; beforeEach(() => { container = document.createElement('div'); navigateToUrl = jest.fn(); }); - it('calls `navigateToUrl` with the link url', () => { - const handler = createHandler(); + it("doesn't call `navigateToUrl` without a container", () => { + const event = createEvent({ + target: createLink({ href: '/base-path/app/targetApp' }), + }); + navigateToUrlClickHandler({ + event, + currentAppId, + container: null, + navigateToUrl, + }); + + expect(event.preventDefault).toHaveBeenCalledTimes(0); + }); + + it("doesn't call `navigateToUrl` without a `currentAppId`", () => { const event = createEvent({ target: createLink({ href: '/base-path/app/targetApp' }), }); + + navigateToUrlClickHandler({ + event, + container, + navigateToUrl, + }); + + expect(event.preventDefault).toHaveBeenCalledTimes(0); + }); + + it('calls `navigateToUrl` with the link url', () => { + const event = createEvent({ + target: createLink({ href: '/base-path/app/targetApp' }), + }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); @@ -71,13 +103,12 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is triggered if a non-link target has a parent link', () => { - const handler = createHandler(); - const link = createLink(); const target = document.createElement('span'); link.appendChild(target); const event = createEvent({ target }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); @@ -85,13 +116,12 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is not triggered if a non-link target has no parent link', () => { - const handler = createHandler(); - const parent = document.createElement('div'); const target = document.createElement('span'); parent.appendChild(target); const event = createEvent({ target }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -99,11 +129,10 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is not triggered when the link has no href', () => { - const handler = createHandler(); - const event = createEvent({ target: createLink({ href: '' }), }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -111,11 +140,10 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is only triggered when the link does not have an external target', () => { - const handler = createHandler(); - let event = createEvent({ target: createLink({ target: '_blank' }), }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -124,6 +152,7 @@ describe('createNavigateToUrlClickHandler', () => { event = createEvent({ target: createLink({ target: 'some-target' }), }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -132,6 +161,7 @@ describe('createNavigateToUrlClickHandler', () => { event = createEvent({ target: createLink({ target: '_self' }), }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); @@ -140,6 +170,7 @@ describe('createNavigateToUrlClickHandler', () => { event = createEvent({ target: createLink({ target: '' }), }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); @@ -147,11 +178,10 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is only triggered from left clicks', () => { - const handler = createHandler(); - let event = createEvent({ button: 1, }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -160,6 +190,7 @@ describe('createNavigateToUrlClickHandler', () => { event = createEvent({ button: 12, }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -168,6 +199,7 @@ describe('createNavigateToUrlClickHandler', () => { event = createEvent({ button: 0, }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); @@ -175,11 +207,10 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is not triggered if the event default is prevented', () => { - const handler = createHandler(); - let event = createEvent({ defaultPrevented: true, }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -188,6 +219,7 @@ describe('createNavigateToUrlClickHandler', () => { event = createEvent({ defaultPrevented: false, }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); @@ -195,15 +227,15 @@ describe('createNavigateToUrlClickHandler', () => { }); it('is not triggered if any modifier key is pressed', () => { - const handler = createHandler(); - let event = createEvent({ modifierKey: true }); + handler(event); expect(event.preventDefault).not.toHaveBeenCalled(); expect(navigateToUrl).not.toHaveBeenCalled(); event = createEvent({ modifierKey: false }); + handler(event); expect(event.preventDefault).toHaveBeenCalledTimes(1); diff --git a/packages/shared-ux/link/redirect_app/src/click_handler.ts b/packages/shared-ux/link/redirect_app/src/click_handler.ts new file mode 100644 index 0000000000000..8c94aa0033f2b --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/click_handler.ts @@ -0,0 +1,57 @@ +/* + * 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 { MouseEvent } from 'react'; +import { getClosestLink, hasActiveModifierKey } from '@kbn/shared-ux-utility'; +import { NavigateToUrl } from './types'; + +interface CreateCrossAppClickHandlerOptions { + event: MouseEvent; + navigateToUrl: NavigateToUrl; + container: HTMLElement | null; + currentAppId?: string; +} + +/** + * Constructs a click handler that will redirect the user using `navigateToUrl` if the + * correct conditions are met. + */ +export const navigateToUrlClickHandler = ({ + event, + container, + navigateToUrl, + currentAppId, +}: CreateCrossAppClickHandlerOptions) => { + if (!container || !currentAppId) { + return; + } + + // see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239 + const target = event.target as HTMLElement; + + const link = getClosestLink(target, container); + + if (!link) { + return; + } + + const isNotEmptyHref = link.href; + const hasNoTarget = link.target === '' || link.target === '_self'; + const isLeftClickOnly = event.button === 0; + + if ( + isNotEmptyHref && + hasNoTarget && + isLeftClickOnly && + !event.defaultPrevented && + !hasActiveModifierKey(event) + ) { + event.preventDefault(); + navigateToUrl(link.href); + } +}; diff --git a/packages/shared-ux/link/redirect_app/src/index.tsx b/packages/shared-ux/link/redirect_app/src/index.tsx new file mode 100644 index 0000000000000..5efb99cc48664 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/index.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_links'; +export { RedirectAppLinks as RedirectAppLinksComponent } from './redirect_app_links'; +export { RedirectAppLinksKibanaProvider, RedirectAppLinksProvider } from './services'; + +import React, { FC } from 'react'; +import { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_links'; +import { + Services, + KibanaServices, + RedirectAppLinksKibanaProvider, + RedirectAppLinksProvider, +} from './services'; + +const isKibanaContract = (services: any): services is KibanaServices => { + return typeof services.coreStart !== 'undefined'; +}; + +/** + * This component composes `RedirectAppLinksContainer` with either `RedirectAppLinksProvider` or + * `RedirectAppLinksKibanaProvider` based on the services provided, creating a single component + * with which consumers can wrap their components or solutions. + */ +export const RedirectAppLinks: FC = ({ children, ...services }) => { + const container = {children}; + + return isKibanaContract(services) ? ( + {container} + ) : ( + {container} + ); +}; diff --git a/packages/shared-ux/link/redirect_app/src/redirect_app_links.component.tsx b/packages/shared-ux/link/redirect_app/src/redirect_app_links.component.tsx new file mode 100644 index 0000000000000..477471fe71824 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/redirect_app_links.component.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useRef, MouseEventHandler, useCallback } from 'react'; +import type { HTMLAttributes, DetailedHTMLProps, FC } from 'react'; + +import { navigateToUrlClickHandler } from './click_handler'; +import { NavigateToUrl } from './types'; + +export interface Props extends DetailedHTMLProps, HTMLDivElement> { + navigateToUrl: NavigateToUrl; + currentAppId?: string | undefined; +} + +/** + * Utility component that will intercept click events on children anchor (``) elements to call + * `navigateToUrl` with the link's href. This will trigger SPA friendly navigation when the link points + * to a valid Kibana app. + * + * @example + * ```tsx + * { ... }}> + * Go to another-app + * + * ``` + */ +export const RedirectAppLinks: FC = ({ + children, + navigateToUrl, + currentAppId, + ...otherProps +}) => { + const containerRef = useRef(null); + + const handleClick: MouseEventHandler = useCallback( + (event) => + navigateToUrlClickHandler({ + event, + currentAppId, + navigateToUrl, + container: containerRef.current, + }), + [currentAppId, navigateToUrl] + ); + + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events +
+ {children} +
+ ); +}; diff --git a/packages/shared-ux/link/redirect_app/src/redirect_app_links.stories.tsx b/packages/shared-ux/link/redirect_app/src/redirect_app_links.stories.tsx new file mode 100644 index 0000000000000..9bb3d0d9782d4 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/redirect_app_links.stories.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { action } from '@storybook/addon-actions'; +import { RedirectAppLinks } from '.'; +import mdx from '../README.mdx'; + +export default { + title: 'Redirect App Links', + description: + 'An "area of effect" component which intercepts clicks on anchor elements and redirects them to Kibana solutions without a page refresh.', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const Component = () => { + const navigateToUrl = async (url: string) => { + action('navigateToUrl')(url); + }; + + const currentAppId = 'abc123'; + + return ( + <> + + + + + Button with URL + + + + + Button without URL + + + + + + + + Button outside RedirectAppLinks + + + + + ); +}; diff --git a/packages/shared-ux/link/redirect_app/src/redirect_app_links.test.tsx b/packages/shared-ux/link/redirect_app/src/redirect_app_links.test.tsx new file mode 100644 index 0000000000000..1bb3875aec7ae --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/redirect_app_links.test.tsx @@ -0,0 +1,292 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import React, { MouseEvent } from 'react'; +import { mount as enzymeMount, ReactWrapper } from 'enzyme'; + +import { RedirectAppLinksKibanaProvider, RedirectAppLinksProvider } from './services'; +import { RedirectAppLinks } from './redirect_app_links'; +import { RedirectAppLinks as ComposedWrapper } from '.'; +import { Observable } from 'rxjs'; + +export type UnmountCallback = () => void; +export type MountPoint = (element: T) => UnmountCallback; +type Mount = ( + node: React.ReactElement +) => ReactWrapper, React.Component<{}, {}, any>>; + +const commonTests = (name: string, mount: Mount, navigateToUrl: jest.Mock) => { + beforeEach(() => { + navigateToUrl.mockReset(); + }); + + describe(`RedirectAppLinks with ${name}`, () => { + it('intercept click events on children link elements', () => { + let event: MouseEvent; + const component = mount( +
{ + event = e; + }} + > + +
+ content +
+
+
+ ); + + component.find('a').simulate('click', { button: 0, defaultPrevented: false }); + expect(navigateToUrl).toHaveBeenCalledTimes(1); + expect(event!.defaultPrevented).toBe(true); + }); + + it('intercept click events on children inside link elements', async () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + + + +
+ ); + + component.find('span').simulate('click', { button: 0, defaultPrevented: false }); + + expect(navigateToUrl).toHaveBeenCalledTimes(1); + expect(event!.defaultPrevented).toBe(true); + }); + + it('does not intercept click events when the target is not inside a link', () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + + + content + + +
+ ); + + component.find('span').simulate('click', { button: 0, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!.defaultPrevented).toBe(false); + }); + + it('does not intercept click events when the link has an external target', () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + + + content + + +
+ ); + + component.find('a').simulate('click', { button: 0, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!.defaultPrevented).toBe(false); + }); + + it('does not intercept click events when the event is already defaultPrevented', () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + + + e.preventDefault()}>content + + +
+ ); + + component.find('span').simulate('click', { button: 0, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!.defaultPrevented).toBe(true); + }); + + it('does not intercept click events when the event propagation is stopped', () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + + e.stopPropagation()}> + content + + +
+ ); + + component.find('a').simulate('click', { button: 0, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!).toBe(undefined); + }); + + it('does not intercept click events when the event is not triggered from the left button', () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + +
+ content +
+
+
+ ); + + component.find('a').simulate('click', { button: 1, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!.defaultPrevented).toBe(false); + }); + + it('does not intercept click events when the event has a modifier key enabled', () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + +
+ content +
+
+
+ ); + + component.find('a').simulate('click', { button: 0, ctrlKey: true, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!.defaultPrevented).toBe(false); + }); + }); +}; + +const targetedTests = (name: string, mount: Mount, navigateToUrl: jest.Mock) => { + beforeEach(() => { + navigateToUrl.mockReset(); + }); + + describe(`${name} with isolated areas of effect`, () => { + it(`does not intercept click events when the link is a parent of the container`, () => { + let event: MouseEvent; + + const component = mount( +
{ + event = e; + }} + > + + + content + + +
+ ); + + component.find('span').simulate('click', { button: 0, defaultPrevented: false }); + + expect(navigateToUrl).not.toHaveBeenCalled(); + expect(event!.defaultPrevented).toBe(false); + }); + }); +}; + +describe('RedirectAppLinks', () => { + const navigateToUrl = jest.fn(); + + beforeEach(() => { + navigateToUrl.mockReset(); + }); + + const kibana = { + coreStart: { + application: { + currentAppId$: new Observable((subscriber) => { + subscriber.next('123'); + }), + navigateToUrl, + }, + }, + }; + + const services = { + currentAppId: 'abc123', + navigateToUrl, + }; + + const provider = (node: React.ReactElement) => + enzymeMount({node}); + + const kibanaProvider = (node: React.ReactElement) => + enzymeMount( + {node} + ); + + const composedProvider = (node: React.ReactElement) => + enzymeMount({node}); + + const composedKibanaProvider = (node: React.ReactElement) => + enzymeMount({node}); + + describe('Test all Providers', () => { + commonTests('RedirectAppLinksProvider', provider, navigateToUrl); + targetedTests('RedirectAppLinksProvider', provider, navigateToUrl); + commonTests('RedirectAppLinksKibanaProvider', kibanaProvider, navigateToUrl); + targetedTests('RedirectAppLinksKibanaProvider', kibanaProvider, navigateToUrl); + commonTests('Provider Props', composedProvider, navigateToUrl); + commonTests('Kibana Props', composedKibanaProvider, navigateToUrl); + }); +}); diff --git a/packages/shared-ux/link/redirect_app/src/redirect_app_links.tsx b/packages/shared-ux/link/redirect_app/src/redirect_app_links.tsx new file mode 100644 index 0000000000000..1e805ad4475b6 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/redirect_app_links.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { useServices } from './services'; +import { + RedirectAppLinks as Component, + Props as ComponentProps, +} from './redirect_app_links.component'; + +type Props = Omit; + +/** + * A service-enabled component that provides Kibana-specific functionality to the `RedirectAppLinks` + * pure component. + * + * @example + * ```tsx + * + * Go to another-app + * + * ``` + */ +export const RedirectAppLinks = (props: Props) => ; diff --git a/packages/shared-ux/link/redirect_app/src/services.tsx b/packages/shared-ux/link/redirect_app/src/services.tsx new file mode 100644 index 0000000000000..22bc5a5cd0c55 --- /dev/null +++ b/packages/shared-ux/link/redirect_app/src/services.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { FC, useContext } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { Observable } from 'rxjs'; +import { NavigateToUrl } from './types'; + +/** + * Contextual services for this component. + */ +export interface Services { + navigateToUrl: NavigateToUrl; + currentAppId?: string; +} + +const RedirectAppLinksContext = React.createContext(null); + +/** + * Contextual services Provider. + */ +export const RedirectAppLinksProvider: FC = ({ children, ...services }) => { + return ( + + {children} + + ); +}; + +/** + * Kibana-specific contextual services to be adapted for this component. + */ +export interface KibanaServices { + coreStart: { + application: { + currentAppId$: Observable; + navigateToUrl: NavigateToUrl; + }; + }; +} + +/** + * Kibana-specific contextual services Provider. + */ +export const RedirectAppLinksKibanaProvider: FC = ({ children, coreStart }) => { + const { navigateToUrl, currentAppId$ } = coreStart.application; + const currentAppId = useObservable(currentAppId$, undefined); + + return ( + + {children} + + ); +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useServices() { + const context = useContext(RedirectAppLinksContext); + + if (!context) { + throw new Error( + 'RedirectAppLinksContext is missing. Ensure your component or React root is wrapped with RedirectAppLinksProvider.' + ); + } + + return context; +} diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/index.tsx b/packages/shared-ux/link/redirect_app/src/types.ts similarity index 73% rename from packages/kbn-shared-ux-components/src/solution_avatar/index.tsx rename to packages/shared-ux/link/redirect_app/src/types.ts index efc597cbdcb13..2c27ccde84d67 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/index.tsx +++ b/packages/shared-ux/link/redirect_app/src/types.ts @@ -5,5 +5,5 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -export { KibanaSolutionAvatar } from './solution_avatar'; -export type { KibanaSolutionAvatarProps } from './solution_avatar'; + +export type NavigateToUrl = (url: string) => Promise | void; diff --git a/packages/shared-ux/link/redirect_app/tsconfig.json b/packages/shared-ux/link/redirect_app/tsconfig.json new file mode 100644 index 0000000000000..93076efae5d7c --- /dev/null +++ b/packages/shared-ux/link/redirect_app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/shared-ux/page/analytics_no_data/BUILD.bazel b/packages/shared-ux/page/analytics_no_data/BUILD.bazel new file mode 100644 index 0000000000000..ad687fe8a220b --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/BUILD.bazel @@ -0,0 +1,139 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") + +PKG_DIRNAME = "analytics_no_data" +PKG_REQUIRE_NAME = "@kbn/shared-ux-page-analytics-no-data" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.mdx", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", +] + +# In this array place runtime dependencies, including other packages and NPM packages +# which must be available for this code to run. +# +# To reference other packages use: +# "//repo/relative/path/to/package" +# eg. "//packages/kbn-utils" +# +# To reference a NPM package use: +# "@npm//name-of-package" +# eg. "@npm//lodash" +RUNTIME_DEPS = [ + "@npm//react", + "@npm//rxjs", + "//packages/kbn-i18n", + "//packages/kbn-shared-ux-services", + "//packages/kbn-shared-ux-components", + "//packages/kbn-shared-ux-storybook" +] + +# In this array place dependencies necessary to build the types, which will include the +# :npm_module_types target of other packages and packages from NPM, including @types/* +# packages. +# +# To reference the types for another package use: +# "//repo/relative/path/to/package:npm_module_types" +# eg. "//packages/kbn-utils:npm_module_types" +# +# References to NPM packages work the same as RUNTIME_DEPS +TYPES_DEPS = [ + "@npm//@types/node", + "@npm//@types/jest", + "@npm//@types/react", + "//packages/kbn-i18n:npm_module_types", + "//packages/kbn-shared-ux-services:npm_module_types", + "//packages/kbn-shared-ux-storybook:npm_module_types", + "//packages/kbn-shared-ux-components:npm_module_types", + "//packages/kbn-ambient-ui-types", +] + +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + web = True, + additional_args = [ + "--copy-files", + "--quiet" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", + ], +) + +ts_project( + name = "tsc_types", + args = ['--pretty'], + srcs = SRCS, + deps = TYPES_DEPS, + declaration = True, + emit_declaration_only = True, + out_dir = "target_types", + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_DIRNAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [":" + PKG_DIRNAME], +) + +filegroup( + name = "build", + srcs = [":npm_module"], + visibility = ["//visibility:public"], +) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [":npm_module_types"], + visibility = ["//visibility:public"], +) diff --git a/packages/shared-ux/page/analytics_no_data/README.mdx b/packages/shared-ux/page/analytics_no_data/README.mdx new file mode 100644 index 0000000000000..ab8cf8d1cb063 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/README.mdx @@ -0,0 +1,16 @@ +--- +id: sharedUX/Components/AnalyticsNoDataPage +slug: /shared-ux/components/analytics-no-data-page +title: Analytics "No Data" Page +summary: An entire page that can be displayed when Kibana "has no data", specifically for Analytics. +tags: ['shared-ux', 'component'] +date: 2021-12-28 +--- + +## Description + +This is an Analytics-specific version of `KibanaNoDataPage`, which defaults most of the fields to give a consistent set of terms for Analytics solutions. + +## EUI Promotion Status + +This component is not currently considered for promotion to EUI. diff --git a/packages/shared-ux/page/analytics_no_data/jest.config.js b/packages/shared-ux/page/analytics_no_data/jest.config.js new file mode 100644 index 0000000000000..76067f82881f7 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/shared-ux/page/analytics_no_data'], +}; diff --git a/packages/shared-ux/page/analytics_no_data/package.json b/packages/shared-ux/page/analytics_no_data/package.json new file mode 100644 index 0000000000000..e9977444fb94e --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/shared-ux-page-analytics-no-data", + "private": true, + "version": "1.0.0", + "main": "./target_node/index.js", + "browser": "./target_web/index.js", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/shared-ux/page/analytics_no_data/src/__snapshots__/analytics_no_data_page.component.test.tsx.snap b/packages/shared-ux/page/analytics_no_data/src/__snapshots__/analytics_no_data_page.component.test.tsx.snap new file mode 100644 index 0000000000000..be6fd3c45744e --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/__snapshots__/analytics_no_data_page.component.test.tsx.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnalyticsNoDataPageComponent renders correctly 1`] = ` + + + + } + > + +
+ + + +
+
+
+
+
+
+`; diff --git a/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.component.test.tsx b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.component.test.tsx new file mode 100644 index 0000000000000..0f18710197991 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.component.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { KibanaNoDataPage } from '@kbn/shared-ux-components'; +import { AnalyticsNoDataPage } from './analytics_no_data_page.component'; + +describe('AnalyticsNoDataPageComponent', () => { + const onDataViewCreated = jest.fn(); + + it('renders correctly', () => { + const component = mountWithIntl( + + ); + expect(component).toMatchSnapshot(); + + expect(component.find(KibanaNoDataPage).length).toBe(1); + + const noDataConfig = component.find(KibanaNoDataPage).props().noDataConfig; + expect(noDataConfig.solution).toEqual('Analytics'); + expect(noDataConfig.pageTitle).toEqual('Welcome to Analytics!'); + expect(noDataConfig.logo).toEqual('logoKibana'); + expect(noDataConfig.docsLink).toEqual('http://www.test.com'); + expect(noDataConfig.action.elasticAgent).not.toBeNull(); + }); +}); diff --git a/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.component.tsx b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.component.tsx new file mode 100644 index 0000000000000..31051328641f4 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.component.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { KibanaNoDataPage } from '@kbn/shared-ux-components'; + +/** + * Props for the pure component. + */ +export interface Props { + kibanaGuideDocLink: string; + onDataViewCreated: (dataView: unknown) => void; +} + +const solution = i18n.translate('sharedUXPackages.noDataConfig.analytics', { + defaultMessage: 'Analytics', +}); + +const pageTitle = i18n.translate('sharedUXPackages.noDataConfig.analyticsPageTitle', { + defaultMessage: 'Welcome to Analytics!', +}); + +const addIntegrationsTitle = i18n.translate('sharedUXPackages.noDataConfig.addIntegrationsTitle', { + defaultMessage: 'Add integrations', +}); + +const addIntegrationsDescription = i18n.translate( + 'sharedUXPackages.noDataConfig.addIntegrationsDescription', + { + defaultMessage: 'Use Elastic Agent to collect data and build out Analytics solutions.', + } +); + +/** + * A pure component of an entire page that can be displayed when Kibana "has no data", specifically for Analytics. + */ +export const AnalyticsNoDataPage = ({ kibanaGuideDocLink, onDataViewCreated }: Props) => { + const noDataConfig = { + solution, + pageTitle, + logo: 'logoKibana', + action: { + elasticAgent: { + title: addIntegrationsTitle, + description: addIntegrationsDescription, + 'data-test-subj': 'kbnOverviewAddIntegrations', + }, + }, + docsLink: kibanaGuideDocLink, + }; + + return ; +}; diff --git a/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.stories.tsx b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.stories.tsx new file mode 100644 index 0000000000000..8471cdf9546d2 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.stories.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { servicesFactory, DataServiceFactoryConfig } from '@kbn/shared-ux-storybook'; + +import { AnalyticsNoDataPage as Component } from './analytics_no_data_page'; +import { AnalyticsNoDataPageProvider, Services } from './services'; +import mdx from '../README.mdx'; + +export default { + title: 'Analytics No Data Page', + description: 'An Analytics-specific version of KibanaNoDataPage.', + parameters: { + docs: { + page: mdx, + }, + }, +}; + +type Params = Pick; + +export const AnalyticsNoDataPage = (params: Params) => { + // Workaround to leverage the services package. + const { application, data, docLinks, editors, http, permissions, platform } = + servicesFactory(params); + + const services: Services = { + ...application, + ...data, + ...docLinks, + ...editors, + ...http, + ...permissions, + ...platform, + kibanaGuideDocLink: 'Kibana guide', + }; + + return ( + + + + ); +}; + +AnalyticsNoDataPage.argTypes = { + hasESData: { + control: 'boolean', + defaultValue: false, + }, + hasUserDataView: { + control: 'boolean', + defaultValue: false, + }, +}; diff --git a/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.test.tsx b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.test.tsx new file mode 100644 index 0000000000000..e091cac70d32b --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { mockServicesFactory } from '@kbn/shared-ux-services'; + +import { Services, AnalyticsNoDataPageProvider } from './services'; +import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component'; +import { AnalyticsNoDataPage } from './analytics_no_data_page'; + +describe('AnalyticsNoDataPage', () => { + const onDataViewCreated = jest.fn(); + + // Workaround to leverage the services package. + const { application, data, docLinks, editors, http, permissions, platform } = + mockServicesFactory(); + + const services: Services = { + ...application, + ...data, + ...docLinks, + ...editors, + ...http, + ...permissions, + ...platform, + kibanaGuideDocLink: 'Kibana guide', + }; + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('renders correctly', async () => { + const component = mountWithIntl( + + + + ); + + expect(component.find(Component).length).toBe(1); + expect(component.find(Component).props().kibanaGuideDocLink).toBe(services.kibanaGuideDocLink); + expect(component.find(Component).props().onDataViewCreated).toBe(onDataViewCreated); + }); +}); diff --git a/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.tsx b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.tsx new file mode 100644 index 0000000000000..141f607a6257e --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/analytics_no_data_page.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +import { LegacyServicesProvider, getLegacyServices } from './legacy_services'; +import { useServices } from './services'; +import { AnalyticsNoDataPage as Component } from './analytics_no_data_page.component'; + +/** + * Props for the `AnalyticsNoDataPage` component. + */ +export interface AnalyticsNoDataPageProps { + onDataViewCreated: (dataView: unknown) => void; +} + +/** + * An entire page that can be displayed when Kibana "has no data", specifically for Analytics. Uses + * services from a provider to provide props to a pure component. + */ +export const AnalyticsNoDataPage = ({ onDataViewCreated }: AnalyticsNoDataPageProps) => { + const services = useServices(); + const { kibanaGuideDocLink } = services; + + return ( + + + + ); +}; diff --git a/packages/shared-ux/page/analytics_no_data/src/index.ts b/packages/shared-ux/page/analytics_no_data/src/index.ts new file mode 100644 index 0000000000000..7b87084f745ef --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +export { AnalyticsNoDataPageProvider, AnalyticsNoDataPageKibanaProvider } from './services'; + +/** + * Lazy-loaded connected component. Must be wrapped in `React.Suspense`. + */ +export const LazyAnalyticsNoDataPage = React.lazy(() => + import('./analytics_no_data_page').then(({ AnalyticsNoDataPage }) => ({ + default: AnalyticsNoDataPage, + })) +); + +/** + * An entire page that can be displayed when Kibana "has no data", specifically for Analytics. + * Requires a Provider for relevant services. + */ +export const AnalyticsNoDataPage = withSuspense(LazyAnalyticsNoDataPage); diff --git a/packages/shared-ux/page/analytics_no_data/src/legacy_services.tsx b/packages/shared-ux/page/analytics_no_data/src/legacy_services.tsx new file mode 100644 index 0000000000000..3d690e56e0d23 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/legacy_services.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { SharedUxServicesProvider as LegacyServicesProvider } from '@kbn/shared-ux-services'; +export type { SharedUxServices as LegacyServices } from '@kbn/shared-ux-services'; + +import { SharedUxServices as LegacyServices } from '@kbn/shared-ux-services'; +import { Services } from './services'; + +/** + * This list is temporary, a stop-gap as we migrate to a package-based architecture, where + * services are not collected in a single package. In order to make the transition, this + * interface is intentionally "flat". + * + * Expect this list to dwindle to zero as `@kbn/shared-ux-components` are migrated to their + * own packages, (and `@kbn/shared-ux-services` is removed). + */ +export const getLegacyServices = (services: Services): LegacyServices => ({ + application: { + currentAppId$: services.currentAppId$, + navigateToUrl: services.navigateToUrl, + }, + data: { + hasESData: services.hasESData, + hasDataView: services.hasDataView, + hasUserDataView: services.hasUserDataView, + }, + docLinks: { + dataViewsDocLink: services.dataViewsDocLink, + }, + editors: { + openDataViewEditor: services.openDataViewEditor, + }, + http: { + addBasePath: services.addBasePath, + }, + permissions: { + canAccessFleet: services.canAccessFleet, + canCreateNewDataView: services.canCreateNewDataView, + }, + platform: { + setIsFullscreen: services.setIsFullscreen, + }, +}); diff --git a/packages/shared-ux/page/analytics_no_data/src/services.tsx b/packages/shared-ux/page/analytics_no_data/src/services.tsx new file mode 100644 index 0000000000000..70ba29ed2f648 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/src/services.tsx @@ -0,0 +1,162 @@ +/* + * 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, { FC, useContext } from 'react'; +import { Observable } from 'rxjs'; + +/** + * TODO: `DataView` is a class exported by `src/plugins/data_views/public`. Since this service + * is contained in this package-- and packages can only depend on other packages and never on + * plugins-- we have to set this to `unknown`. If and when `DataView` is exported from a + * stateless package, we can remove this. + * + * @see: https://github.com/elastic/kibana/issues/127695 + */ +type DataView = unknown; + +/** + * A subset of the `DataViewEditorOptions` interface relevant to this component. + * + * @see: src/plugins/data_view_editor/public/types.ts + */ +interface DataViewEditorOptions { + /** Handler to be invoked when the Data View Editor completes a save operation. */ + onSave: (dataView: DataView) => void; +} + +/** + * A list of Services that are consumed by this component. + * + * This list is temporary, a stopgap as we migrate to a package-based architecture, where + * services are not collected in a single package. In order to make the transition, this + * interface is intentionally "flat". + * + * Expect this list to dwindle to zero as `@kbn/shared-ux-components` are migrated to their + * own packages, (and `@kbn/shared-ux-services` is removed). + */ +export interface Services { + addBasePath: (url: string) => string; + canAccessFleet: boolean; + canCreateNewDataView: boolean; + currentAppId$: Observable; + dataViewsDocLink: string; + hasDataView: () => Promise; + hasESData: () => Promise; + hasUserDataView: () => Promise; + kibanaGuideDocLink: string; + navigateToUrl: (url: string) => Promise; + openDataViewEditor: (options: DataViewEditorOptions) => () => void; + setIsFullscreen: (isFullscreen: boolean) => void; +} + +const AnalyticsNoDataPageContext = React.createContext(null); + +/** + * A Context Provider that provides services to the component. + */ +export const AnalyticsNoDataPageProvider: FC = ({ children, ...services }) => { + return ( + + {children} + + ); +}; + +/** + * An interface containing a collection of Kibana plugins and services required to + * render this component and its dependencies. + */ +export interface AnalyticsNoDataPageKibanaDependencies { + coreStart: { + application: { + capabilities: { + navLinks: { + integrations: boolean; + }; + }; + currentAppId$: Observable; + navigateToUrl: (url: string) => Promise; + }; + chrome: { + setIsVisible: (isVisible: boolean) => void; + }; + docLinks: { + links: { + indexPatterns: { + introduction: string; + }; + kibana: { + guide: string; + }; + }; + }; + http: { + basePath: { + prepend: (url: string) => string; + }; + }; + }; + dataViews: { + hasData: { + hasDataView: () => Promise; + hasESData: () => Promise; + hasUserDataView: () => Promise; + }; + }; + dataViewEditor: { + openEditor: (options: DataViewEditorOptions) => () => void; + userPermissions: { + editDataView: () => boolean; + }; + }; +} + +/** + * Kibana-specific Provider that maps dependencies to services. + */ +export const AnalyticsNoDataPageKibanaProvider: FC = ({ + children, + ...dependencies +}) => { + const { coreStart, dataViewEditor, dataViews } = dependencies; + const value: Services = { + addBasePath: coreStart.http.basePath.prepend, + canAccessFleet: coreStart.application.capabilities.navLinks.integrations, + canCreateNewDataView: dataViewEditor.userPermissions.editDataView(), + currentAppId$: coreStart.application.currentAppId$, + dataViewsDocLink: coreStart.docLinks.links.indexPatterns?.introduction, + hasDataView: dataViews.hasData.hasDataView, + hasESData: dataViews.hasData.hasESData, + hasUserDataView: dataViews.hasData.hasUserDataView, + kibanaGuideDocLink: coreStart.docLinks.links.kibana.guide, + navigateToUrl: coreStart.application.navigateToUrl, + openDataViewEditor: dataViewEditor.openEditor, + setIsFullscreen: (isVisible: boolean) => coreStart.chrome.setIsVisible(isVisible), + }; + + return ( + + {children} + + ); +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useServices() { + const context = useContext(AnalyticsNoDataPageContext); + + if (!context) { + throw new Error( + 'AnalyticsNoDataPageContext is missing. Ensure your component or React root is wrapped with AnalyticsNoDataPageContext.' + ); + } + + return context; +} diff --git a/packages/shared-ux/page/analytics_no_data/tsconfig.json b/packages/shared-ux/page/analytics_no_data/tsconfig.json new file mode 100644 index 0000000000000..573ad07325100 --- /dev/null +++ b/packages/shared-ux/page/analytics_no_data/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.bazel.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "target_types", + "rootDir": "src", + "stripInternal": false, + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types", + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/src/core/server/saved_objects/migrations/actions/initialize_action.test.ts b/src/core/server/saved_objects/migrations/actions/initialize_action.test.ts index 7c75470b890aa..5d831a5bb8f78 100644 --- a/src/core/server/saved_objects/migrations/actions/initialize_action.test.ts +++ b/src/core/server/saved_objects/migrations/actions/initialize_action.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import * as Either from 'fp-ts/lib/Either'; import { catchRetryableEsClientErrors } from './catch_retryable_es_client_errors'; import { errors as EsErrors } from '@elastic/elasticsearch'; jest.mock('./catch_retryable_es_client_errors'); @@ -16,16 +17,16 @@ describe('initAction', () => { beforeEach(() => { jest.clearAllMocks(); }); - const retryableError = new EsErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: 503, - body: { error: { type: 'es_type', reason: 'es_reason' } }, - }) - ); - const client = elasticsearchClientMock.createInternalClient( - elasticsearchClientMock.createErrorTransportRequestPromise(retryableError) - ); it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const retryableError = new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 503, + body: { error: { type: 'es_type', reason: 'es_reason' } }, + }) + ); + const client = elasticsearchClientMock.createInternalClient( + elasticsearchClientMock.createErrorTransportRequestPromise(retryableError) + ); const task = initAction({ client, indices: ['my_index'] }); try { await task(); @@ -34,4 +35,88 @@ describe('initAction', () => { } expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); + it('resolves right when persistent and transient cluster settings are compatible', async () => { + const clusterSettingsResponse = { + transient: { 'cluster.routing.allocation.enable': 'all' }, + persistent: { 'cluster.routing.allocation.enable': 'all' }, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isRight(result)).toEqual(true); + }); + it('resolves right when persistent and transient cluster settings are undefined', async () => { + const clusterSettingsResponse = { + transient: {}, + persistent: {}, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isRight(result)).toEqual(true); + }); + it('resolves right when persistent cluster settings are compatible', async () => { + const clusterSettingsResponse = { + transient: {}, + persistent: { 'cluster.routing.allocation.enable': 'all' }, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isRight(result)).toEqual(true); + }); + it('resolves right when transient cluster settings are compatible', async () => { + const clusterSettingsResponse = { + transient: { 'cluster.routing.allocation.enable': 'all' }, + persistent: {}, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isRight(result)).toEqual(true); + }); + it('resolves right when valid transient settings, incompatible persistent settings', async () => { + const clusterSettingsResponse = { + transient: { 'cluster.routing.allocation.enable': 'all' }, + persistent: { 'cluster.routing.allocation.enable': 'primaries' }, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isRight(result)).toEqual(true); + }); + it('resolves left when valid persistent settings, incompatible transient settings', async () => { + const clusterSettingsResponse = { + transient: { 'cluster.routing.allocation.enable': 'primaries' }, + persistent: { 'cluster.routing.allocation.enable': 'alls' }, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isLeft(result)).toEqual(true); + }); + it('resolves left when transient cluster settings are incompatible', async () => { + const clusterSettingsResponse = { + transient: { 'cluster.routing.allocation.enable': 'none' }, + persistent: { 'cluster.routing.allocation.enable': 'all' }, + }; + const client = elasticsearchClientMock.createInternalClient( + new Promise((res) => res(clusterSettingsResponse)) + ); + const task = initAction({ client, indices: ['my_index'] }); + const result = await task(); + expect(Either.isLeft(result)).toEqual(true); + }); }); diff --git a/src/core/server/saved_objects/migrations/actions/initialize_action.ts b/src/core/server/saved_objects/migrations/actions/initialize_action.ts index 281e3a0a4f3e0..e7f011cb4c5f2 100644 --- a/src/core/server/saved_objects/migrations/actions/initialize_action.ts +++ b/src/core/server/saved_objects/migrations/actions/initialize_action.ts @@ -44,16 +44,15 @@ export const checkClusterRoutingAllocationEnabledTask = flat_settings: true, }) .then((settings) => { - const clusterRoutingAllocations: string[] = + // transient settings take preference over persistent settings + const clusterRoutingAllocation = settings?.transient?.[routingAllocationEnable] ?? - settings?.persistent?.[routingAllocationEnable] ?? - []; + settings?.persistent?.[routingAllocationEnable]; - const clusterRoutingAllocationEnabled = - [...clusterRoutingAllocations].length === 0 || - [...clusterRoutingAllocations].every((s: string) => s === 'all'); // if set, only allow 'all' + const clusterRoutingAllocationEnabledIsAll = + clusterRoutingAllocation === undefined || clusterRoutingAllocation === 'all'; - if (!clusterRoutingAllocationEnabled) { + if (!clusterRoutingAllocationEnabledIsAll) { return Either.left({ type: 'unsupported_cluster_routing_allocation' as const, message: diff --git a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts index 9846e5f48dc21..cddd2f323f1fc 100644 --- a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts @@ -116,7 +116,7 @@ describe('migration actions', () => { await client.cluster.putSettings({ body: { persistent: { - // Remove persistent test settings + // Reset persistent test settings cluster: { routing: { allocation: { enable: null } } }, }, }, @@ -126,11 +126,11 @@ describe('migration actions', () => { expect.assertions(1); const task = initAction({ client, indices: ['no_such_index'] }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": Object {}, - } - `); + Object { + "_tag": "Right", + "right": Object {}, + } + `); }); it('resolves right record with found indices', async () => { expect.assertions(1); @@ -149,7 +149,7 @@ describe('migration actions', () => { }) ); }); - it('resolves left with cluster routing allocation disabled', async () => { + it('resolves left when cluster.routing.allocation.enabled is incompatible', async () => { expect.assertions(3); await client.cluster.putSettings({ body: { @@ -164,14 +164,14 @@ describe('migration actions', () => { indices: ['existing_index_with_docs'], }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "message": "[unsupported_cluster_routing_allocation] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue.", - "type": "unsupported_cluster_routing_allocation", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "message": "[unsupported_cluster_routing_allocation] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue.", + "type": "unsupported_cluster_routing_allocation", + }, + } + `); await client.cluster.putSettings({ body: { persistent: { @@ -185,14 +185,14 @@ describe('migration actions', () => { indices: ['existing_index_with_docs'], }); await expect(task2()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "message": "[unsupported_cluster_routing_allocation] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue.", - "type": "unsupported_cluster_routing_allocation", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "message": "[unsupported_cluster_routing_allocation] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue.", + "type": "unsupported_cluster_routing_allocation", + }, + } + `); await client.cluster.putSettings({ body: { persistent: { @@ -206,14 +206,30 @@ describe('migration actions', () => { indices: ['existing_index_with_docs'], }); await expect(task3()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "message": "[unsupported_cluster_routing_allocation] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue.", - "type": "unsupported_cluster_routing_allocation", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "message": "[unsupported_cluster_routing_allocation] The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue.", + "type": "unsupported_cluster_routing_allocation", + }, + } + `); + }); + it('resolves right when cluster.routing.allocation.enabled=all', async () => { + expect.assertions(1); + await client.cluster.putSettings({ + body: { + persistent: { + cluster: { routing: { allocation: { enable: 'all' } } }, + }, + }, + }); + const task = initAction({ + client, + indices: ['existing_index_with_docs'], + }); + const result = await task(); + expect(Either.isRight(result)).toBe(true); }); }); @@ -271,14 +287,14 @@ describe('migration actions', () => { expect.assertions(1); const task = setWriteBlock({ client, index: 'no_such_index' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "index": "no_such_index", - "type": "index_not_found_exception", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "index": "no_such_index", + "type": "index_not_found_exception", + }, + } + `); }); }); @@ -300,21 +316,21 @@ describe('migration actions', () => { expect.assertions(1); const task = removeWriteBlock({ client, index: 'existing_index_with_write_block_2' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "remove_write_block_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "remove_write_block_succeeded", + } + `); }); it('resolves right if successful when an index does not have a write block', async () => { expect.assertions(1); const task = removeWriteBlock({ client, index: 'existing_index_without_write_block_2' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "remove_write_block_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "remove_write_block_succeeded", + } + `); }); it('rejects if there is a non-retryable error', async () => { expect.assertions(1); @@ -398,14 +414,14 @@ describe('migration actions', () => { timeout: '1s', }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "message": "[index_not_yellow_timeout] Timeout waiting for the status of the [red_index] index to become 'yellow'", - "type": "index_not_yellow_timeout", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "message": "[index_not_yellow_timeout] Timeout waiting for the status of the [red_index] index to become 'yellow'", + "type": "index_not_yellow_timeout", + }, + } + `); }); }); @@ -425,14 +441,14 @@ describe('migration actions', () => { }); expect.assertions(1); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": Object { - "acknowledged": true, - "shardsAcknowledged": true, - }, - } - `); + Object { + "_tag": "Right", + "right": Object { + "acknowledged": true, + "shardsAcknowledged": true, + }, + } + `); }); it('resolves right after waiting for index status to be yellow if clone target already existed', async () => { expect.assertions(2); @@ -491,14 +507,14 @@ describe('migration actions', () => { expect.assertions(1); const task = cloneIndex({ client, source: 'no_such_index', target: 'clone_target_3' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "index": "no_such_index", - "type": "index_not_found_exception", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "index": "no_such_index", + "type": "index_not_found_exception", + }, + } + `); }); it('resolves left with a index_not_yellow_timeout if clone target already exists but takes longer than the specified timeout before turning yellow', async () => { // Create a red index @@ -527,14 +543,14 @@ describe('migration actions', () => { })(); await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "message": "[index_not_yellow_timeout] Timeout waiting for the status of the [clone_red_index] index to become 'yellow'", - "type": "index_not_yellow_timeout", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "message": "[index_not_yellow_timeout] Timeout waiting for the status of the [clone_red_index] index to become 'yellow'", + "type": "index_not_yellow_timeout", + }, + } + `); // Now that we know timeouts work, make the index yellow again and call cloneIndex a second time to verify that it completes @@ -555,14 +571,14 @@ describe('migration actions', () => { })(); await expect(cloneIndexPromise2).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": Object { - "acknowledged": true, - "shardsAcknowledged": true, - }, - } - `); + Object { + "_tag": "Right", + "right": Object { + "acknowledged": true, + "shardsAcknowledged": true, + }, + } + `); }); }); @@ -580,11 +596,11 @@ describe('migration actions', () => { })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "reindex_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "reindex_succeeded", + } + `); const results = ( (await searchForOutdatedDocuments(client, { @@ -620,11 +636,11 @@ describe('migration actions', () => { })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "reindex_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "reindex_succeeded", + } + `); const results = ( (await searchForOutdatedDocuments(client, { @@ -653,11 +669,11 @@ describe('migration actions', () => { })()) as Either.Right; const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "reindex_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "reindex_succeeded", + } + `); const results = ( (await searchForOutdatedDocuments(client, { batchSize: 1000, diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index 88240429856d1..49967feb214d6 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -65,16 +65,16 @@ export const CreateDockerUbuntu: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'x64', + baseImage: 'ubuntu', context: false, image: true, - ubuntu: true, dockerBuildDate, }); await runDockerGenerator(config, log, build, { architecture: 'aarch64', + baseImage: 'ubuntu', context: false, image: true, - ubuntu: true, dockerBuildDate, }); }, @@ -86,8 +86,8 @@ export const CreateDockerUBI: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'x64', + baseImage: 'ubi', context: false, - ubi: true, image: true, }); }, @@ -99,16 +99,16 @@ export const CreateDockerCloud: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'x64', + baseImage: 'ubuntu', context: false, cloud: true, - ubuntu: true, image: true, }); await runDockerGenerator(config, log, build, { architecture: 'aarch64', + baseImage: 'ubuntu', context: false, cloud: true, - ubuntu: true, image: true, }); }, @@ -119,23 +119,25 @@ export const CreateDockerContexts: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { - ubuntu: true, + baseImage: 'ubuntu', context: true, image: false, dockerBuildDate, }); await runDockerGenerator(config, log, build, { - ubi: true, + baseImage: 'ubi', context: true, image: false, }); await runDockerGenerator(config, log, build, { ironbank: true, + baseImage: 'none', context: true, image: false, }); await runDockerGenerator(config, log, build, { + baseImage: 'ubuntu', cloud: true, context: true, image: false, diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 264c6e52db0eb..d8b604f00b46e 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -29,22 +29,21 @@ export async function runDockerGenerator( build: Build, flags: { architecture?: string; + baseImage: 'none' | 'ubi' | 'ubuntu'; context: boolean; image: boolean; - ubi?: boolean; - ubuntu?: boolean; ironbank?: boolean; cloud?: boolean; dockerBuildDate?: string; } ) { - let baseOSImage = ''; - if (flags.ubuntu) baseOSImage = 'ubuntu:20.04'; - if (flags.ubi) baseOSImage = 'docker.elastic.co/ubi8/ubi-minimal:latest'; + let baseImageName = ''; + if (flags.baseImage === 'ubuntu') baseImageName = 'ubuntu:20.04'; + if (flags.baseImage === 'ubi') baseImageName = 'docker.elastic.co/ubi8/ubi-minimal:latest'; const ubiVersionTag = 'ubi8'; let imageFlavor = ''; - if (flags.ubi) imageFlavor += `-${ubiVersionTag}`; + if (flags.baseImage === 'ubi') imageFlavor += `-${ubiVersionTag}`; if (flags.ironbank) imageFlavor += '-ironbank'; if (flags.cloud) imageFlavor += '-cloud'; @@ -61,7 +60,6 @@ export async function runDockerGenerator( const artifactsDir = config.resolveFromTarget('.'); const beatsDir = config.resolveFromRepo('.beats'); const dockerBuildDate = flags.dockerBuildDate || new Date().toISOString(); - // That would produce oss, default and default-ubi7 const dockerBuildDir = config.resolveFromRepo('build', 'kibana-docker', `default${imageFlavor}`); const imageArchitecture = flags.architecture === 'aarch64' ? '-aarch64' : ''; const dockerTargetFilename = config.resolveFromTarget( @@ -93,10 +91,9 @@ export async function runDockerGenerator( dockerPush, dockerTagQualifier, dockerCrossCompile, - baseOSImage, + baseImageName, dockerBuildDate, - ubi: flags.ubi, - ubuntu: flags.ubuntu, + baseImage: flags.baseImage, cloud: flags.cloud, metricbeatTarball, filebeatTarball, diff --git a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts index 35977d47aaaa7..32a551820a05b 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts @@ -20,12 +20,11 @@ export interface TemplateContext { imageTag: string; dockerBuildDir: string; dockerTargetFilename: string; - baseOSImage: string; dockerBuildDate: string; usePublicArtifact?: boolean; publicArtifactSubdomain: string; - ubi?: boolean; - ubuntu?: boolean; + baseImage: 'none' | 'ubi' | 'ubuntu'; + baseImageName: string; cloud?: boolean; metricbeatTarball?: string; filebeatTarball?: string; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index 95f6a56ef68cb..d171c48662cf6 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -9,7 +9,7 @@ # Build stage 0 `builder`: # Extract Kibana artifact ################################################################################ -FROM {{{baseOSImage}}} AS builder +FROM {{{baseImageName}}} AS builder {{#ubi}} RUN {{packageManager}} install -y findutils tar gzip @@ -54,7 +54,7 @@ RUN mkdir -p /opt/filebeat /opt/metricbeat && \ # Copy kibana from stage 0 # Add entrypoint ################################################################################ -FROM {{{baseOSImage}}} +FROM {{{baseImageName}}} EXPOSE 5601 {{#ubi}} diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 316428d46a957..472e64e849b58 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -18,7 +18,7 @@ function generator({ dockerCrossCompile, version, dockerTargetFilename, - baseOSImage, + baseImageName, architecture, }: TemplateContext) { const dockerTargetName = `${imageTag}${imageFlavor}:${version}${ @@ -61,7 +61,7 @@ function generator({ done } - retry_docker_pull ${baseOSImage} + retry_docker_pull ${baseImageName} echo "Building: kibana${imageFlavor}-docker"; \\ ${dockerBuild} diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts index 94068f2b64b12..63b04ed6f70b0 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts @@ -16,7 +16,9 @@ function generator(options: TemplateContext) { const dir = options.ironbank ? 'ironbank' : 'base'; const template = readFileSync(resolve(__dirname, dir, './Dockerfile')); return Mustache.render(template.toString(), { - packageManager: options.ubi ? 'microdnf' : 'apt-get', + packageManager: options.baseImage === 'ubi' ? 'microdnf' : 'apt-get', + ubi: options.baseImage === 'ubi', + ubuntu: options.baseImage === 'ubuntu', ...options, }); } diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 8aa2d6f1cfe55..ced601d0f3981 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -146,10 +146,10 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'x-pack/plugins/monitoring/public/icons/health-green.svg', 'x-pack/plugins/monitoring/public/icons/health-red.svg', 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf', - 'x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png', + 'x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf', + 'x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf', + 'x-pack/plugins/screenshotting/server/assets/img/logo-grey.png', ]; diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx index 555df3c2c5c11..0e67d787be144 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.test.tsx @@ -388,7 +388,7 @@ describe('Field', () => { const updated = wrapper.update(); findTestSubject(updated, `advancedSetting-resetField-${setting.name}`).simulate('click'); expect(handleChange).toBeCalledWith(setting.name, { - value: getEditableValue(setting.type, setting.defVal), + value: getEditableValue(setting.type, setting.defVal, setting.defVal), changeImage: true, }); }); diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index fd4674a7caf6e..56673cda1a953 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -100,7 +100,7 @@ export class Field extends PureComponent { if (type === 'image') { this.cancelChangeImage(); return this.handleChange({ - value: getEditableValue(type, defVal), + value: getEditableValue(type, defVal, defVal), changeImage: true, }); } diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts index 49446310a894b..5a1dad533c084 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/common_data_layer_args.ts @@ -6,13 +6,17 @@ * Side Public License, v 1. */ +import { ArgumentType } from '@kbn/expressions-plugin/common'; import { SeriesTypes, XScaleTypes, YScaleTypes, Y_CONFIG } from '../constants'; import { strings } from '../i18n'; -import { DataLayerFn, ExtendedDataLayerFn } from '../types'; +import { DataLayerArgs, ExtendedDataLayerArgs } from '../types'; -type CommonDataLayerFn = DataLayerFn | ExtendedDataLayerFn; +type CommonDataLayerArgs = ExtendedDataLayerArgs | DataLayerArgs; +type CommonDataLayerFnArgs = { + [key in keyof CommonDataLayerArgs]: ArgumentType; +}; -export const commonDataLayerArgs: CommonDataLayerFn['args'] = { +export const commonDataLayerArgs: CommonDataLayerFnArgs = { hide: { types: ['boolean'], default: false, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.test.ts deleted file mode 100644 index 518690d47bfcb..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.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 { DataLayerArgs } from '../types'; -import { createMockExecutionContext } from '@kbn/expressions-plugin/common/mocks'; -import { mockPaletteOutput, sampleArgs } from '../__mocks__'; -import { LayerTypes } from '../constants'; -import { dataLayerFunction } from './data_layer'; - -describe('dataLayerConfig', () => { - test('produces the correct arguments', () => { - const { data } = sampleArgs(); - const args: DataLayerArgs = { - seriesType: 'line', - xAccessor: 'c', - accessors: ['a', 'b'], - splitAccessor: 'd', - xScaleType: 'linear', - yScaleType: 'linear', - isHistogram: false, - palette: mockPaletteOutput, - }; - - const result = dataLayerFunction.fn(data, args, createMockExecutionContext()); - - expect(result).toEqual({ - type: 'dataLayer', - layerType: LayerTypes.DATA, - ...args, - table: data, - }); - }); -}); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts deleted file mode 100644 index f36a0ea4c101f..0000000000000 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/data_layer.ts +++ /dev/null @@ -1,30 +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 { DataLayerFn } from '../types'; -import { DATA_LAYER, LayerTypes } from '../constants'; -import { strings } from '../i18n'; -import { commonDataLayerArgs } from './common_data_layer_args'; - -export const dataLayerFunction: DataLayerFn = { - name: DATA_LAYER, - aliases: [], - type: DATA_LAYER, - help: strings.getDataLayerFnHelp(), - inputTypes: ['datatable'], - args: { ...commonDataLayerArgs }, - fn(table, args) { - return { - type: DATA_LAYER, - ...args, - accessors: args.accessors ?? [], - layerType: LayerTypes.DATA, - table, - }; - }, -}; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts index ab1d570a07351..30a76217b5c0e 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/index.ts @@ -13,7 +13,6 @@ export * from './annotation_layer'; export * from './extended_annotation_layer'; export * from './y_axis_config'; export * from './extended_y_axis_config'; -export * from './data_layer'; export * from './extended_data_layer'; export * from './grid_lines_config'; export * from './axis_extent_config'; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index 688efbe122f3e..871135dd45bcb 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -15,16 +15,22 @@ describe('xyVis', () => { test('it renders with the specified data and args', async () => { const { data, args } = sampleArgs(); const { layers, ...rest } = args; + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; const result = await xyVisFunction.fn( data, - { ...rest, dataLayers: [sampleLayer], referenceLineLayers: [], annotationLayers: [] }, + { ...rest, ...restLayerArgs, referenceLineLayers: [], annotationLayers: [] }, createMockExecutionContext() ); expect(result).toEqual({ type: 'render', as: XY_VIS, - value: { args: { ...rest, layers: [sampleLayer] } }, + value: { + args: { + ...rest, + layers: [{ layerType, table: data, layerId: 'dataLayers-0', type, ...restLayerArgs }], + }, + }, }); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts index 2e97cb00b3e55..e8a5858d3ed26 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.ts @@ -7,9 +7,10 @@ */ import { XyVisFn } from '../types'; -import { XY_VIS, DATA_LAYER, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants'; +import { XY_VIS, REFERENCE_LINE_LAYER, ANNOTATION_LAYER } from '../constants'; import { strings } from '../i18n'; import { commonXYArgs } from './common_xy_args'; +import { commonDataLayerArgs } from './common_data_layer_args'; export const xyVisFunction: XyVisFn = { name: XY_VIS, @@ -18,11 +19,7 @@ export const xyVisFunction: XyVisFn = { help: strings.getXYHelp(), args: { ...commonXYArgs, - dataLayers: { - types: [DATA_LAYER], - help: strings.getDataLayerHelp(), - multi: true, - }, + ...commonDataLayerArgs, referenceLineLayers: { types: [REFERENCE_LINE_LAYER], help: strings.getReferenceLineLayerHelp(), diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 1bd75e1296c6c..6ce3116d377e1 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -7,9 +7,9 @@ */ import { Dimension, prepareLogTable } from '@kbn/visualizations-plugin/common/utils'; -import { LayerTypes, XY_VIS_RENDERER } from '../constants'; +import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER } from '../constants'; import { appendLayerIds } from '../helpers'; -import { XYLayerConfig, XyVisFn } from '../types'; +import { DataLayerConfigResult, XYLayerConfig, XyVisFn } from '../types'; import { getLayerDimensions } from '../utils'; import { hasAreaLayer, @@ -21,7 +21,40 @@ import { } from './validate'; export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { - const { dataLayers = [], referenceLineLayers = [], annotationLayers = [], ...restArgs } = args; + const { + referenceLineLayers = [], + annotationLayers = [], + seriesType, + accessors = [], + xAccessor, + hide, + splitAccessor, + columnToLabel, + yScaleType, + xScaleType, + isHistogram, + yConfig, + palette, + ...restArgs + } = args; + const dataLayers: DataLayerConfigResult[] = [ + { + type: DATA_LAYER, + seriesType, + accessors, + xAccessor, + hide, + splitAccessor, + columnToLabel, + yScaleType, + xScaleType, + isHistogram, + palette, + yConfig, + layerType: LayerTypes.DATA, + table: data, + }, + ]; const layers: XYLayerConfig[] = [ ...appendLayerIds(dataLayers, 'dataLayers'), ...appendLayerIds(referenceLineLayers, 'referenceLineLayers'), diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index 71786eeba9b36..86eb173d4d4ed 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -181,7 +181,7 @@ export interface LabelsOrientationConfig { } // Arguments to XY chart expression, with computed properties -export interface XYArgs { +export interface XYArgs extends DataLayerArgs { xTitle: string; yTitle: string; yRightTitle: string; @@ -191,7 +191,6 @@ export interface XYArgs { endValue?: EndValue; emphasizeFitting?: boolean; valueLabels: ValueLabelMode; - dataLayers: DataLayerConfigResult[]; referenceLineLayers: ReferenceLineLayerConfigResult[]; annotationLayers: AnnotationLayerConfigResult[]; fittingFunction?: FittingFunction; @@ -386,12 +385,6 @@ export type LayeredXyVisFn = ExpressionFunctionDefinition< Promise >; -export type DataLayerFn = ExpressionFunctionDefinition< - typeof DATA_LAYER, - Datatable, - DataLayerArgs, - DataLayerConfigResult ->; export type ExtendedDataLayerFn = ExpressionFunctionDefinition< typeof EXTENDED_DATA_LAYER, Datatable, diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index 5e68d2c621894..5c27da6b82b28 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -17,7 +17,6 @@ import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './t import { xyVisFunction, layeredXyVisFunction, - dataLayerFunction, extendedDataLayerFunction, yAxisConfigFunction, extendedYAxisConfigFunction, @@ -59,7 +58,6 @@ export class ExpressionXyPlugin { expressions.registerFunction(extendedYAxisConfigFunction); expressions.registerFunction(legendConfigFunction); expressions.registerFunction(gridlinesConfigFunction); - expressions.registerFunction(dataLayerFunction); expressions.registerFunction(extendedDataLayerFunction); expressions.registerFunction(axisExtentConfigFunction); expressions.registerFunction(tickLabelsConfigFunction); diff --git a/src/plugins/chart_expressions/expression_xy/server/plugin.ts b/src/plugins/chart_expressions/expression_xy/server/plugin.ts index 37252a7296580..cefde5d38a5f4 100755 --- a/src/plugins/chart_expressions/expression_xy/server/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/server/plugin.ts @@ -15,7 +15,6 @@ import { extendedYAxisConfigFunction, legendConfigFunction, gridlinesConfigFunction, - dataLayerFunction, axisExtentConfigFunction, tickLabelsConfigFunction, annotationLayerFunction, @@ -37,7 +36,6 @@ export class ExpressionXyPlugin expressions.registerFunction(extendedYAxisConfigFunction); expressions.registerFunction(legendConfigFunction); expressions.registerFunction(gridlinesConfigFunction); - expressions.registerFunction(dataLayerFunction); expressions.registerFunction(extendedDataLayerFunction); expressions.registerFunction(axisExtentConfigFunction); expressions.registerFunction(tickLabelsConfigFunction); diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f3aedf36b66a7..c95a2308c3965 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -119,13 +119,12 @@ export function DashboardApp({ <> {isCompleteDashboardAppState(dashboardAppState) && ( <> - {!printMode && ( - - )} + {dashboardAppState.savedDashboard.outcome === 'conflict' && dashboardAppState.savedDashboard.id && diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss index a451178cc46b0..cd9c41f392a0b 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss +++ b/src/plugins/dashboard/public/application/embeddable/viewport/_print_viewport.scss @@ -1,9 +1,68 @@ -.printViewport { - &__vis { - height: 600px; // These values might need to be passed in as dimensions for the report. I.e., print should use layout dimensions. - width: 975px; +@import './print_media/styling/index'; - // Some vertical space between vis, but center horizontally - margin: 10px auto; +$visualisationsPerPage: 2; +$visPadding: 4mm; + +/* +We set the same visual padding on the browser and print versions of the UI so that +we don't hit a race condition where padding is being updated while the print image +is being formed. This can result in parts of the vis being cut out. +*/ +@mixin visualizationPadding { + // Open space from page margin + padding-left: $visPadding; + padding-right: $visPadding; + + // Last vis on the page + &:nth-child(#{$visualisationsPerPage}n) { + page-break-after: always; + padding-top: $visPadding; + padding-bottom: $visPadding; + } + + &:last-child { + page-break-after: avoid; + } +} + +@media screen, projection { + .printViewport { + &__vis { + @include visualizationPadding(); + + & .embPanel__header button { + display: none; + } + + margin: $euiSizeL auto; + height: calc(#{$a4PageContentHeight} / #{$visualisationsPerPage}); + width: $a4PageContentWidth; + padding: $visPadding; + } + } +} + +@media print { + .printViewport { + &__vis { + @include visualizationPadding(); + + height: calc(#{$a4PageContentHeight} / #{$visualisationsPerPage}); + width: $a4PageContentWidth; + + & .euiPanel { + box-shadow: none !important; + } + + & .embPanel__header button { + display: none; + } + + page-break-inside: avoid; + + & * { + overflow: hidden !important; + } + } } } diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/print_media/README.md b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/README.md new file mode 100644 index 0000000000000..9bd8bfc3a0944 --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/README.md @@ -0,0 +1,7 @@ +# Print media + +The code here is designed to be movable outside the domain of Dashboard. Currently, +the components and styles are only used by Dashboard but we may choose to move them to, +for example, a Kibana package in the future. + +Any changes to this code must be tested by generating a print-optimized PDF in dashboard. \ No newline at end of file diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_index.scss b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_index.scss new file mode 100644 index 0000000000000..16c4dd85ea45e --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_index.scss @@ -0,0 +1,52 @@ +@import './vars'; + +/* +This styling contains utility and minimal layout styles to help plugins create +print-ready HTML. + +Observations: +1. We currently do not control the user-agent's header and footer content + (including the style of fonts) for client-side printing. + +2. Page box model is quite different from what we have in browsers - page + margins define where the "no-mans-land" exists for actual content. Moving + content into this space by, for example setting negative margins resulted + in slightly unpredictable behaviour because the browser wants to either + move this content to another page or it may get split across two + pages. + +3. page-break-* is your friend! +*/ + +// Currently we cannot control or style the content the browser places in +// margins, this might change in the future: +// See https://drafts.csswg.org/css-page-3/#margin-boxes +@page { + size: A4; + orientation: portrait; + margin: 0; + margin-top: $a4PageHeaderHeight; + margin-bottom: $a4PageFooterHeight; +} + +@media print { + + html { + background-color: #FFF; + } + + // It is good practice to show the full URL in the final, printed output + a[href]:after { + content: ' [' attr(href) ']'; + } + + figure { + page-break-inside: avoid; + } + + * { + -webkit-print-color-adjust: exact !important; /* Chrome, Safari, Edge */ + color-adjust: exact !important; /*Firefox*/ + } + +} diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_vars.scss b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_vars.scss new file mode 100644 index 0000000000000..d7addc7afb261 --- /dev/null +++ b/src/plugins/dashboard/public/application/embeddable/viewport/print_media/styling/_vars.scss @@ -0,0 +1,10 @@ + +$a4PageHeight: 297mm; +$a4PageWidth: 210mm; +$a4PageMargin: 0; +$a4PagePadding: 0; +$a4PageHeaderHeight: 15mm; +$a4PageFooterHeight: 20mm; + +$a4PageContentHeight: $a4PageHeight - $a4PageHeaderHeight - $a4PageFooterHeight; +$a4PageContentWidth: $a4PageWidth; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index a103c88843664..50c40e4863bee 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -183,8 +183,7 @@ export const useDashboardAppState = ({ savedDashboard, }); - // Backwards compatible way of detecting that we are taking a screenshot - const legacyPrintLayoutDetected = + const printLayoutDetected = screenshotModeService?.isScreenshotMode() && screenshotModeService.getScreenshotContext('layout') === 'print'; @@ -194,8 +193,7 @@ export const useDashboardAppState = ({ ...initialDashboardStateFromUrl, ...forwardedAppState, - // if we are in legacy print mode, dashboard needs to be in print viewMode - ...(legacyPrintLayoutDetected ? { viewMode: ViewMode.PRINT } : {}), + ...(printLayoutDetected ? { viewMode: ViewMode.PRINT } : {}), // if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it. ...(incomingEmbeddable ? { viewMode: ViewMode.EDIT } : {}), diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 7095ad34cd189..5cbbd30c79a24 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -82,6 +82,7 @@ export interface DashboardTopNavProps { dashboardAppState: CompleteDashboardAppState; embedSettings?: DashboardEmbedSettings; redirectTo: DashboardRedirect; + printMode: boolean; } const LabsFlyout = withSuspense(LazyLabsFlyout, null); @@ -90,6 +91,7 @@ export function DashboardTopNav({ dashboardAppState, embedSettings, redirectTo, + printMode, }: DashboardTopNavProps) { const { core, @@ -488,7 +490,9 @@ export function DashboardTopNav({ const isFullScreenMode = dashboardState.fullScreenMode; const showTopNavMenu = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowTopNavMenu)); - const showQueryInput = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowQueryInput)); + const showQueryInput = shouldShowNavBarComponent( + Boolean(embedSettings?.forceShowQueryInput || printMode) + ); const showDatePicker = shouldShowNavBarComponent(Boolean(embedSettings?.forceShowDatePicker)); const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar)); const showQueryBar = showQueryInput || showDatePicker || showFilterBar; @@ -535,6 +539,7 @@ export function DashboardTopNav({ useDefaultBehaviors: true, savedQuery: state.savedQuery, savedQueryId: dashboardState.savedQuery, + visible: printMode !== true, onQuerySubmit: (_payload, isUpdate) => { if (isUpdate === false) { dashboardAppState.$triggerDashboardRefresh.next({ force: true }); @@ -585,10 +590,10 @@ export function DashboardTopNav({ return ( <> - {isLabsEnabled && isLabsShown ? ( + {!printMode && isLabsEnabled && isLabsShown ? ( setIsLabsShown(false)} /> ) : null} - {dashboardState.viewMode !== ViewMode.VIEW ? ( + {dashboardState.viewMode !== ViewMode.VIEW && !printMode ? ( <> diff --git a/src/plugins/kibana_usage_collection/README.md b/src/plugins/kibana_usage_collection/README.md index 4ea014457fd07..08e830fba4155 100644 --- a/src/plugins/kibana_usage_collection/README.md +++ b/src/plugins/kibana_usage_collection/README.md @@ -2,17 +2,18 @@ This plugin registers the Platform Usage Collectors in Kibana. -| Collector name | Description | Extended documentation | -|----------------|:------------|:----------------------:| -| **Application Usage** | Measures how popular an App in Kibana is by reporting the on-screen time and the number of general clicks that happen in it. | [Link](./server/collectors/application_usage/README.md) | -| **Core Metrics** | Collects the usage reported by the core APIs | - | -| **Config Usage** | Reports the non-default values set via `kibana.yml` config file or CLI options. It `[redacts]` any potential PII-sensitive values. | [Link](./server/collectors/config_usage/README.md) | -| **User-changed UI Settings** | Reports all the UI Settings that have been overwritten by the user. It `[redacts]` any potential PII-sensitive values. | [Link](./server/collectors/management/README.md) | -| **CSP configuration** | Reports the key values regarding the CSP configuration. | - | -| **Kibana** | It reports the number of Saved Objects per type. It is limited to `dashboard`, `visualization`, `search`, `index-pattern`, `graph-workspace`.
It exists for legacy purposes, and may still be used by Monitoring via Metricbeat. | - | -| **Saved Objects Counts** | Number of Saved Objects per type. | - | -| **Localization data** | Localization settings: setup locale and installed translation files. | - | -| **Ops stats** | Operation metrics from the system. | - | -| **UI Counters** | Daily aggregation of the number of times an event occurs in the UI. | [Link](../usage_collection/README.mdx#ui-counters) | -| **UI Metrics** | Deprecated. Old form of UI Counters. It reports the _count of the repetitions since the cluster's first start_ of any UI events that may have happened. | - | -| **Usage Counters** | Daily aggregation of the number of times an event occurs on the Server. | [Link](../usage_collection/README.mdx#usage-counters) | +| Collector name | Description | Extended documentation | +|--------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------:| +| **Application Usage** | Measures how popular an App in Kibana is by reporting the on-screen time and the number of general clicks that happen in it. | [Link](./server/collectors/application_usage/README.md) | +| **Core Metrics** | Collects the usage reported by the core APIs | - | +| **Config Usage** | Reports the non-default values set via `kibana.yml` config file or CLI options. It `[redacts]` any potential PII-sensitive values. | [Link](./server/collectors/config_usage/README.md) | +| **User-changed UI Settings** | Reports all the UI Settings that have been overwritten by the user. It `[redacts]` any potential PII-sensitive values. | [Link](./server/collectors/management/README.md) | +| **CSP configuration** | Reports the key values regarding the CSP configuration. | - | +| **Kibana** | It reports the number of Saved Objects per type. It is limited to `dashboard`, `visualization`, `search`, `index-pattern`, `graph-workspace`.
It exists for legacy purposes, and may still be used by Monitoring via Metricbeat. | - | +| **Saved Objects Counts** | Number of Saved Objects per type. | - | +| **Localization data** | Localization settings: setup locale and installed translation files. | - | +| **Ops stats** | Operation metrics from the system. | - | +| **UI Counters** | Daily aggregation of the number of times an event occurs in the UI. | [Link](../usage_collection/README.mdx#ui-counters) | +| **UI Metrics** | Deprecated. Old form of UI Counters. It reports the _count of the repetitions since the cluster's first start_ of any UI events that may have happened. | - | +| **Usage Counters** | Daily aggregation of the number of times an event occurs on the Server. | [Link](../usage_collection/README.mdx#usage-counters) | +| **Event-based Telemetry Success Counters** | Using the UI and Usage Counters APIs, it reports the stats coming out of the `core.analytics.telemetryCounters$` observable. | [Browser](./public/ebt_counters/README.md) and [Server](./server/ebt_counters/README.md) | diff --git a/src/plugins/kibana_usage_collection/kibana.json b/src/plugins/kibana_usage_collection/kibana.json index 39b55e5c6dd94..41fc5c6c37b78 100644 --- a/src/plugins/kibana_usage_collection/kibana.json +++ b/src/plugins/kibana_usage_collection/kibana.json @@ -6,7 +6,7 @@ }, "version": "kibana", "server": true, - "ui": false, + "ui": true, "requiredPlugins": [ "usageCollection" ], diff --git a/src/plugins/kibana_usage_collection/public/ebt_counters/README.md b/src/plugins/kibana_usage_collection/public/ebt_counters/README.md new file mode 100644 index 0000000000000..d30aa0661e977 --- /dev/null +++ b/src/plugins/kibana_usage_collection/public/ebt_counters/README.md @@ -0,0 +1,14 @@ +# Event-based Telemetry Success Counters (browser-side) + +Using the UI Counters API, it reports the stats coming from the `core.analytics.telemetryCounters$` observable. It allows us to track the success of the EBT client on the browser. + +## Field mappings + +As the number of fields available in the Usage API is reduced, this collection merges some fields to be able to report it. + +| UI Counter field | Telemetry Counter fields | +|------------------|--------------------------------------------------------------------------------------------------| +| `appName` | Concatenation of the string `'ebt_counters.'` and the `source` (`'client'` or the shipper name). | +| `eventName` | Matches the `eventType`. | +| `counterType` | Concatenation of the `type` and the `code` (i.e.: `'succeeded_200'`). | +| `total` | Matches the value in `count`. | \ No newline at end of file diff --git a/src/plugins/kibana_usage_collection/public/ebt_counters/index.ts b/src/plugins/kibana_usage_collection/public/ebt_counters/index.ts new file mode 100644 index 0000000000000..24deee4afb5d0 --- /dev/null +++ b/src/plugins/kibana_usage_collection/public/ebt_counters/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerEbtCounters } from './register_ebt_counters'; diff --git a/src/plugins/kibana_usage_collection/public/ebt_counters/register_ebt_counters.test.ts b/src/plugins/kibana_usage_collection/public/ebt_counters/register_ebt_counters.test.ts new file mode 100644 index 0000000000000..2bf67d02fe110 --- /dev/null +++ b/src/plugins/kibana_usage_collection/public/ebt_counters/register_ebt_counters.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { TelemetryCounter } from '@kbn/analytics-client'; +import { TelemetryCounterType } from '@kbn/analytics-client'; +import { coreMock } from '@kbn/core/public/mocks'; +import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; +import { registerEbtCounters } from './register_ebt_counters'; + +describe('registerEbtCounters', () => { + let core: ReturnType; + let usageCollection: ReturnType; + let internalListener: (counter: TelemetryCounter) => void; + let telemetryCounter$Spy: jest.SpyInstance; + + beforeEach(() => { + core = coreMock.createSetup(); + usageCollection = usageCollectionPluginMock.createSetupContract(); + telemetryCounter$Spy = jest + .spyOn(core.analytics.telemetryCounter$, 'subscribe') + .mockImplementation(((listener) => { + internalListener = listener as (counter: TelemetryCounter) => void; + }) as typeof core.analytics.telemetryCounter$['subscribe']); + }); + + test('it subscribes to `analytics.telemetryCounters$`', () => { + registerEbtCounters(core.analytics, usageCollection); + expect(telemetryCounter$Spy).toHaveBeenCalledTimes(1); + }); + + test('it reports a UI counter whenever a counter is emitted', () => { + registerEbtCounters(core.analytics, usageCollection); + expect(telemetryCounter$Spy).toHaveBeenCalledTimes(1); + internalListener({ + type: TelemetryCounterType.succeeded, + source: 'test-shipper', + event_type: 'test-event', + code: 'test-code', + count: 1, + }); + expect(usageCollection.reportUiCounter).toHaveBeenCalledTimes(1); + expect(usageCollection.reportUiCounter).toHaveBeenCalledWith( + 'ebt_counters.test-shipper', + 'succeeded_test-code', + 'test-event', + 1 + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/public/ebt_counters/register_ebt_counters.ts b/src/plugins/kibana_usage_collection/public/ebt_counters/register_ebt_counters.ts new file mode 100644 index 0000000000000..483e00d8d03fe --- /dev/null +++ b/src/plugins/kibana_usage_collection/public/ebt_counters/register_ebt_counters.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { AnalyticsServiceSetup } from '@kbn/core/public'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; + +export function registerEbtCounters( + analytics: AnalyticsServiceSetup, + usageCollection: UsageCollectionSetup +) { + // The client should complete telemetryCounter$ when shutting down. We shouldn't need to pipe(takeUntil(stop$)). + analytics.telemetryCounter$.subscribe(({ type, source, event_type: eventType, code, count }) => { + usageCollection.reportUiCounter(`ebt_counters.${source}`, `${type}_${code}`, eventType, count); + }); +} diff --git a/src/plugins/kibana_usage_collection/public/index.ts b/src/plugins/kibana_usage_collection/public/index.ts new file mode 100644 index 0000000000000..5474b8db0b27f --- /dev/null +++ b/src/plugins/kibana_usage_collection/public/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { KibanaUsageCollectionPlugin } from './plugin'; + +export function plugin() { + return new KibanaUsageCollectionPlugin(); +} diff --git a/src/plugins/kibana_usage_collection/public/plugin.ts b/src/plugins/kibana_usage_collection/public/plugin.ts new file mode 100644 index 0000000000000..2b7a4b868b76a --- /dev/null +++ b/src/plugins/kibana_usage_collection/public/plugin.ts @@ -0,0 +1,25 @@ +/* + * 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 { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import type { CoreSetup, Plugin } from '@kbn/core/public'; +import { registerEbtCounters } from './ebt_counters'; + +interface KibanaUsageCollectionPluginsDepsSetup { + usageCollection: UsageCollectionSetup; +} + +export class KibanaUsageCollectionPlugin implements Plugin { + public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { + registerEbtCounters(coreSetup.analytics, usageCollection); + } + + public start() {} + + public stop() {} +} diff --git a/src/plugins/kibana_usage_collection/server/ebt_counters/README.md b/src/plugins/kibana_usage_collection/server/ebt_counters/README.md new file mode 100644 index 0000000000000..46762148a952c --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/ebt_counters/README.md @@ -0,0 +1,14 @@ +# Event-based Telemetry Success Counters (server-side) + +Using the Usage Counters API, it reports the stats coming from the `core.analytics.telemetryCounters$` observable. It allows us to track the success of the EBT client on the server side. + +## Field mappings + +As the number of fields available in the Usage API is reduced, this collection merges some fields to be able to report it. + +| Usage Counter field | Telemetry Counter fields | +|---------------------|--------------------------------------------------------------------------------------------------| +| `domainId` | Concatenation of the string `'ebt_counters.'` and the `source` (`'client'` or the shipper name). | +| `counterName` | Matches the `eventType`. | +| `counterType` | Concatenation of the `type` and the `code` (i.e.: `'succeeded_200'`). | +| `total` | Matches the value in `count`. | \ No newline at end of file diff --git a/src/plugins/kibana_usage_collection/server/ebt_counters/index.ts b/src/plugins/kibana_usage_collection/server/ebt_counters/index.ts new file mode 100644 index 0000000000000..24deee4afb5d0 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/ebt_counters/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { registerEbtCounters } from './register_ebt_counters'; diff --git a/src/plugins/kibana_usage_collection/server/ebt_counters/register_ebt_counters.test.ts b/src/plugins/kibana_usage_collection/server/ebt_counters/register_ebt_counters.test.ts new file mode 100644 index 0000000000000..ddeb85ee1be02 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/ebt_counters/register_ebt_counters.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { TelemetryCounter } from '@kbn/analytics-client'; +import { TelemetryCounterType } from '@kbn/analytics-client'; +import { coreMock } from '@kbn/core/server/mocks'; +import { createUsageCollectionSetupMock } from '@kbn/usage-collection-plugin/server/mocks'; +import { registerEbtCounters } from './register_ebt_counters'; + +describe('registerEbtCounters', () => { + let core: ReturnType; + let usageCollection: ReturnType; + let internalListener: (counter: TelemetryCounter) => void; + let telemetryCounter$Spy: jest.SpyInstance; + + beforeEach(() => { + core = coreMock.createSetup(); + usageCollection = createUsageCollectionSetupMock(); + telemetryCounter$Spy = jest + .spyOn(core.analytics.telemetryCounter$, 'subscribe') + .mockImplementation(((listener) => { + internalListener = listener as (counter: TelemetryCounter) => void; + }) as typeof core.analytics.telemetryCounter$['subscribe']); + }); + + test('it subscribes to `analytics.telemetryCounters$`', () => { + registerEbtCounters(core.analytics, usageCollection); + expect(telemetryCounter$Spy).toHaveBeenCalledTimes(1); + }); + + test('it creates a new usageCounter when it does not exist', () => { + registerEbtCounters(core.analytics, usageCollection); + expect(telemetryCounter$Spy).toHaveBeenCalledTimes(1); + internalListener({ + type: TelemetryCounterType.succeeded, + source: 'test-shipper', + event_type: 'test-event', + code: 'test-code', + count: 1, + }); + expect(usageCollection.getUsageCounterByType).toHaveBeenCalledTimes(1); + expect(usageCollection.getUsageCounterByType).toHaveBeenCalledWith('ebt_counters.test-shipper'); + expect(usageCollection.createUsageCounter).toHaveBeenCalledTimes(1); + expect(usageCollection.createUsageCounter).toHaveBeenCalledWith('ebt_counters.test-shipper'); + }); + + test('it reuses the usageCounter when it already exists', () => { + const incrementCounterMock = jest.fn(); + usageCollection.getUsageCounterByType.mockReturnValue({ + incrementCounter: incrementCounterMock, + }); + registerEbtCounters(core.analytics, usageCollection); + expect(telemetryCounter$Spy).toHaveBeenCalledTimes(1); + internalListener({ + type: TelemetryCounterType.succeeded, + source: 'test-shipper', + event_type: 'test-event', + code: 'test-code', + count: 1, + }); + expect(usageCollection.getUsageCounterByType).toHaveBeenCalledTimes(1); + expect(usageCollection.getUsageCounterByType).toHaveBeenCalledWith('ebt_counters.test-shipper'); + expect(usageCollection.createUsageCounter).toHaveBeenCalledTimes(0); + expect(incrementCounterMock).toHaveBeenCalledTimes(1); + expect(incrementCounterMock).toHaveBeenCalledWith({ + counterName: 'test-event', + counterType: `succeeded_test-code`, + incrementBy: 1, + }); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/ebt_counters/register_ebt_counters.ts b/src/plugins/kibana_usage_collection/server/ebt_counters/register_ebt_counters.ts new file mode 100644 index 0000000000000..ed2100dccf929 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/ebt_counters/register_ebt_counters.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { AnalyticsServiceSetup } from '@kbn/core/server'; +import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; + +export function registerEbtCounters( + analytics: AnalyticsServiceSetup, + usageCollection: UsageCollectionSetup +) { + // The client should complete telemetryCounter$ when shutting down. We shouldn't need to pipe(takeUntil(stop$)). + analytics.telemetryCounter$.subscribe(({ type, source, event_type: eventType, code, count }) => { + // We create one counter per source ('client'|). + const domainId = `ebt_counters.${source}`; + const usageCounter = + usageCollection.getUsageCounterByType(domainId) ?? + usageCollection.createUsageCounter(domainId); + + usageCounter.incrementCounter({ + counterName: eventType, // the name of the event + counterType: `${type}_${code}`, // e.g. 'succeeded_200' + incrementBy: count, + }); + }); +} diff --git a/src/plugins/kibana_usage_collection/server/mocks.ts b/src/plugins/kibana_usage_collection/server/plugin.test.mocks.ts similarity index 83% rename from src/plugins/kibana_usage_collection/server/mocks.ts rename to src/plugins/kibana_usage_collection/server/plugin.test.mocks.ts index 7df27a3719e92..a21b2b007f5e9 100644 --- a/src/plugins/kibana_usage_collection/server/mocks.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.mocks.ts @@ -16,3 +16,9 @@ export const detectCloudServiceMock = mock.detectCloudService; jest.doMock('./collectors/cloud/detector', () => ({ CloudDetector: jest.fn().mockImplementation(() => mock), })); + +export const registerEbtCountersMock = jest.fn(); + +jest.doMock('./ebt_counters', () => ({ + registerEbtCounters: registerEbtCountersMock, +})); diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index a6604ac0bc1cd..ef26492c2d6fd 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -15,7 +15,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '@kbn/usage-collection-plugin/server/mocks'; -import { cloudDetailsMock } from './mocks'; +import { cloudDetailsMock, registerEbtCountersMock } from './plugin.test.mocks'; import { plugin } from '.'; describe('kibana_usage_collection', () => { @@ -44,6 +44,9 @@ describe('kibana_usage_collection', () => { expect(coreSetup.coreUsageData.registerUsageCounter).toHaveBeenCalled(); + expect(registerEbtCountersMock).toHaveBeenCalledTimes(1); + expect(registerEbtCountersMock).toHaveBeenCalledWith(coreSetup.analytics, usageCollection); + await expect( Promise.all( usageCollectors.map(async (usageCollector) => { diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 34bf029311307..10f05ccbac945 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -21,6 +21,7 @@ import type { CoreUsageDataStart, } from '@kbn/core/server'; import { SavedObjectsClient, EventLoopDelaysMonitor } from '@kbn/core/server'; +import { registerEbtCounters } from './ebt_counters'; import { startTrackingEventLoopDelaysUsage, startTrackingEventLoopDelaysThreshold, @@ -68,6 +69,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { } public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { + registerEbtCounters(coreSetup.analytics, usageCollection); usageCollection.createUsageCounter('uiCounters'); this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop'); coreSetup.coreUsageData.registerUsageCounter(usageCollection.createUsageCounter('core')); diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index e57d6e25db8cd..d9a1e648995bb 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -9,6 +9,7 @@ }, "include": [ "common/*", + "public/**/**/*", "server/**/**/*", "../../../typings/*" ], diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index db6cf1bc3d006..5ae2a4498b55b 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -4,6 +4,12 @@ } } +.kbnTopNavMenu__wrapper { + &--hidden { + display: none; + } +} + .kbnTopNavMenu__badgeWrapper { display: flex; align-items: baseline; diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 62dc67a3ee941..86c83a6b48be5 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -28,6 +28,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & showFilterBar?: boolean; unifiedSearch?: UnifiedSearchPublicPluginStart; className?: string; + visible?: boolean; /** * If provided, the menu part of the component will be rendered as a portal inside the given mount point. * @@ -105,9 +106,11 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null { } function renderLayout() { - const { setMenuMountPoint } = props; + const { setMenuMountPoint, visible } = props; const menuClassName = classNames('kbnTopNavMenu', props.className); - const wrapperClassName = 'kbnTopNavMenu__wrapper'; + const wrapperClassName = classNames('kbnTopNavMenu__wrapper', { + 'kbnTopNavMenu__wrapper--hidden': visible === false, + }); if (setMenuMountPoint) { return ( <> diff --git a/src/plugins/newsfeed/README.md b/src/plugins/newsfeed/README.md index d8a0bffb4ed0b..398578092425d 100644 --- a/src/plugins/newsfeed/README.md +++ b/src/plugins/newsfeed/README.md @@ -1,4 +1,4 @@ # newsfeed The newsfeed plugin adds a NewsfeedNavButton to the top navigation bar and renders the content in the flyout. -Content is fetched from the remote (https://feeds.elastic.co and https://feeds-staging.elastic.co in dev mode) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely. +Content is fetched from the remote (https://feeds.elastic.co) once a day, with periodic checks if the content needs to be refreshed. All newsfeed content is hosted remotely. diff --git a/src/plugins/newsfeed/common/constants.ts b/src/plugins/newsfeed/common/constants.ts index 6ba5e07ea873e..f4467dfe35011 100644 --- a/src/plugins/newsfeed/common/constants.ts +++ b/src/plugins/newsfeed/common/constants.ts @@ -8,5 +8,4 @@ export const NEWSFEED_FALLBACK_LANGUAGE = 'en'; export const NEWSFEED_DEFAULT_SERVICE_BASE_URL = 'https://feeds.elastic.co'; -export const NEWSFEED_DEV_SERVICE_BASE_URL = 'https://feeds-staging.elastic.co'; export const NEWSFEED_DEFAULT_SERVICE_PATH = '/kibana/v{VERSION}.json'; diff --git a/src/plugins/newsfeed/server/config.ts b/src/plugins/newsfeed/server/config.ts index f14f3452761e1..f371da244f871 100644 --- a/src/plugins/newsfeed/server/config.ts +++ b/src/plugins/newsfeed/server/config.ts @@ -10,19 +10,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { NEWSFEED_DEFAULT_SERVICE_PATH, NEWSFEED_DEFAULT_SERVICE_BASE_URL, - NEWSFEED_DEV_SERVICE_BASE_URL, } from '../common/constants'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), service: schema.object({ pathTemplate: schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_PATH }), - urlRoot: schema.conditional( - schema.contextRef('prod'), - schema.literal(true), // Point to staging if it's not a production release - schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_BASE_URL }), - schema.string({ defaultValue: NEWSFEED_DEV_SERVICE_BASE_URL }) - ), + urlRoot: schema.string({ defaultValue: NEWSFEED_DEFAULT_SERVICE_BASE_URL }), }), mainInterval: schema.duration({ defaultValue: '2m' }), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote fetchInterval: schema.duration({ defaultValue: '1d' }), // (1day) How often to fetch remote and reset the last fetched time diff --git a/src/plugins/unified_search/kibana.json b/src/plugins/unified_search/kibana.json index b947141a0c68a..07e438ab52174 100755 --- a/src/plugins/unified_search/kibana.json +++ b/src/plugins/unified_search/kibana.json @@ -9,7 +9,7 @@ }, "server": true, "ui": true, - "requiredPlugins": ["dataViews", "data", "uiActions"], + "requiredPlugins": ["dataViews", "data", "uiActions", "screenshotMode"], "requiredBundles": ["kibanaUtils", "kibanaReact", "data"], "serviceFolders": ["autocomplete"], "configPath": ["unifiedSearch"] diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts index 26727b56094a0..08f07e507d96b 100755 --- a/src/plugins/unified_search/public/plugin.ts +++ b/src/plugins/unified_search/public/plugin.ts @@ -55,7 +55,7 @@ export class UnifiedSearchPublicPlugin public start( core: CoreStart, - { data, dataViews, uiActions }: UnifiedSearchStartDependencies + { data, dataViews, uiActions, screenshotMode }: UnifiedSearchStartDependencies ): UnifiedSearchPublicPluginStart { setTheme(core.theme); setOverlays(core.overlays); @@ -68,6 +68,7 @@ export class UnifiedSearchPublicPlugin data, storage: this.storage, usageCollection: this.usageCollection, + isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()), }); uiActions.addTriggerAction( diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx index 7a04b92c7e063..3d41a9145dffa 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx @@ -120,7 +120,7 @@ describe('Querybar Menu component', () => { expect(component.find('[data-test-subj="queryBarMenuPanel"]')).toBeTruthy(); }); - it('should render the saved filter sets panels if the showQueryInput prop is true but disabled', async () => { + it('should render the saved saved queries panels if the showQueryInput prop is true but disabled', async () => { const newProps = { ...props, openQueryBarMenu: true, @@ -140,7 +140,7 @@ describe('Querybar Menu component', () => { expect(loadFilterSetButton.first().prop('disabled')).toBe(true); }); - it('should render the filter sets panels if the showFilterBar is true but disabled', async () => { + it('should render the saved queries panels if the showFilterBar is true but disabled', async () => { const newProps = { ...props, openQueryBarMenu: true, diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx index 2b34aef33eeee..810d0a64d0251 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx @@ -94,7 +94,7 @@ export function QueryBarMenu({ }; const buttonLabel = i18n.translate('unifiedSearch.filter.options.filterSetButtonLabel', { - defaultMessage: 'Filter set menu', + defaultMessage: 'Saved query menu', }); const button = ( diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx index de70e66fda5fc..548d7f24d5da7 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx @@ -249,10 +249,10 @@ export function QueryBarMenuPanels({ { name: savedQuery ? i18n.translate('unifiedSearch.filter.options.loadOtherFilterSetLabel', { - defaultMessage: 'Load other filter set', + defaultMessage: 'Load other saved query', }) : i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { - defaultMessage: 'Load filter set', + defaultMessage: 'Load saved query', }), panel: 4, width: 350, @@ -266,7 +266,7 @@ export function QueryBarMenuPanels({ defaultMessage: 'Save as new', }) : i18n.translate('unifiedSearch.filter.options.saveFilterSetLabel', { - defaultMessage: 'Save filter set', + defaultMessage: 'Save saved query', }), icon: 'save', disabled: @@ -331,7 +331,13 @@ export function QueryBarMenuPanels({ size="s" data-test-subj="savedQueryTitle" > - {savedQuery ? savedQuery.attributes.title : 'Filter set'} + + {savedQuery + ? savedQuery.attributes.title + : i18n.translate('unifiedSearch.search.searchBar.savedQuery', { + defaultMessage: 'Saved query', + })} +
{savedQuery && savedQueryHasChanged && Boolean(showSaveQuery) && hasFiltersOrQuery && ( @@ -397,7 +403,7 @@ export function QueryBarMenuPanels({ { id: 1, title: i18n.translate('unifiedSearch.filter.options.saveCurrentFilterSetLabel', { - defaultMessage: 'Save current filter set', + defaultMessage: 'Save current saved query', }), disabled: !Boolean(showSaveQuery), content:
{saveAsNewQueryFormComponent}
, @@ -483,7 +489,7 @@ export function QueryBarMenuPanels({ { id: 4, title: i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { - defaultMessage: 'Load filter set', + defaultMessage: 'Load saved query', }), width: 400, content:
{manageFilterSetComponent}
, diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index bb01338d8d5a0..0bff12ac78798 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -87,6 +87,7 @@ export interface QueryBarTopRowProps { filterBar?: React.ReactNode; showDatePickerAsBadge?: boolean; showSubmitButton?: boolean; + isScreenshotMode?: boolean; } const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -474,6 +475,8 @@ export const QueryBarTopRow = React.memo( ); } + const isScreenshotMode = props.isScreenshotMode === true; + return ( <> - - {renderDataViewsPicker()} - - {renderQueryInput()} - - {shouldShowDatePickerAsBadge() && props.filterBar} - {renderUpdateButton()} - - {!shouldShowDatePickerAsBadge() && props.filterBar} + {!isScreenshotMode && ( + <> + + {renderDataViewsPicker()} + + {renderQueryInput()} + + {shouldShowDatePickerAsBadge() && props.filterBar} + {renderUpdateButton()} + + {!shouldShowDatePickerAsBadge() && props.filterBar} + + )} ); }, diff --git a/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx b/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx index 186c1f072aedd..008c61b909ce1 100644 --- a/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx +++ b/src/plugins/unified_search/public/saved_query_form/save_query_form.tsx @@ -203,7 +203,7 @@ export function SaveQueryForm({ disabled={hasErrors} > {i18n.translate('unifiedSearch.search.searchBar.savedQueryFormSaveButtonText', { - defaultMessage: 'Save filter set', + defaultMessage: 'Save saved query', })} diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx index 7c2d0ebd1faad..c7db17ea934d5 100644 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx @@ -140,7 +140,7 @@ describe('Saved query management list component', () => { .find('[data-test-subj="saved-query-management-apply-changes-button"]') .first() .text() - ).toBe('Apply filter set'); + ).toBe('Apply saved query'); const newProps = { ...props, @@ -153,7 +153,7 @@ describe('Saved query management list component', () => { .find('[data-test-subj="saved-query-management-apply-changes-button"]') .first() .text() - ).toBe('Replace with selected filter set'); + ).toBe('Replace with selected saved query'); }); it('should render the modal on delete', async () => { diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx index 7568bb9375fa6..127aa804f77f8 100644 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx @@ -266,7 +266,7 @@ export function SavedQueryManagementList({ placeholder: i18n.translate( 'unifiedSearch.query.queryBar.indexPattern.findFilterSet', { - defaultMessage: 'Find a filter set', + defaultMessage: 'Find a saved query', } ), }} @@ -323,7 +323,7 @@ export function SavedQueryManagementList({ aria-label={i18n.translate( 'unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel', { - defaultMessage: 'Apply filter set', + defaultMessage: 'Apply saved query', } )} data-test-subj="saved-query-management-apply-changes-button" @@ -332,13 +332,13 @@ export function SavedQueryManagementList({ ? i18n.translate( 'unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel', { - defaultMessage: 'Replace with selected filter set', + defaultMessage: 'Replace with selected saved query', } ) : i18n.translate( 'unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel', { - defaultMessage: 'Apply filter set', + defaultMessage: 'Apply saved query', } )} diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx index c4e54995b5979..c73aa258863ed 100644 --- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx @@ -26,6 +26,7 @@ interface StatefulSearchBarDeps { data: Omit; storage: IStorageWrapper; usageCollection?: UsageCollectionSetup; + isScreenshotMode?: boolean; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -110,7 +111,13 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) { +export function createSearchBar({ + core, + storage, + data, + usageCollection, + isScreenshotMode = false, +}: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { @@ -197,6 +204,7 @@ export function createSearchBar({ core, storage, data, usageCollection }: Statef {...overrideDefaultBehaviors(props)} dataViewPickerComponentProps={props.dataViewPickerComponentProps} displayStyle={props.displayStyle} + isScreenshotMode={isScreenshotMode} /> ); diff --git a/src/plugins/unified_search/public/search_bar/search_bar.styles.ts b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts index 1072a684eeaad..36d06d1cb9c7f 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.styles.ts +++ b/src/plugins/unified_search/public/search_bar/search_bar.styles.ts @@ -20,5 +20,8 @@ export const searchBarStyles = ({ euiTheme }: UseEuiTheme) => { inPage: css` padding: 0; `, + hidden: css` + display: none; + `, }; }; diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index a684e5ba928a8..8c5abc1bf4c2c 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -87,6 +87,7 @@ export interface SearchBarOwnProps { fillSubmitButton?: boolean; dataViewPickerComponentProps?: DataViewPickerProps; showSubmitButton?: boolean; + isScreenshotMode?: boolean; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -341,13 +342,16 @@ class SearchBarUI extends Component { public render() { const { theme } = this.props; + const isScreenshotMode = this.props.isScreenshotMode === true; const styles = searchBarStyles(theme); const cssStyles = [ styles.uniSearchBar, this.props.displayStyle && styles[this.props.displayStyle], + isScreenshotMode && styles.hidden, ]; const classes = classNames('uniSearchBar', { + [`uniSearchBar--hidden`]: isScreenshotMode, [`uniSearchBar--${this.props.displayStyle}`]: this.props.displayStyle, }); @@ -470,6 +474,7 @@ class SearchBarUI extends Component { dataViewPickerComponentProps={this.props.dataViewPickerComponentProps} showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()} filterBar={filterBar} + isScreenshotMode={this.props.isScreenshotMode} /> ); diff --git a/src/plugins/unified_search/public/types.ts b/src/plugins/unified_search/public/types.ts index 29cf59f41a871..fa0fc9e826e37 100755 --- a/src/plugins/unified_search/public/types.ts +++ b/src/plugins/unified_search/public/types.ts @@ -8,6 +8,7 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; @@ -29,6 +30,7 @@ export interface UnifiedSearchStartDependencies { fieldFormats: FieldFormatsStart; data: DataPublicPluginStart; uiActions: UiActionsStart; + screenshotMode?: ScreenshotModePluginStart; } /** diff --git a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts index 58d8de723639d..eb6c8fb0888f2 100644 --- a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts +++ b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const ebtUIHelper = getService('kibana_ebt_ui'); const { common } = getPageObjects(['common']); - describe('Core Context Providers', () => { + // FLAKY: https://github.com/elastic/kibana/issues/131729 + describe.skip('Core Context Providers', () => { let event: Event; before(async () => { await common.navigateToApp('home'); diff --git a/test/common/fixtures/plugins/newsfeed/server/plugin.ts b/test/common/fixtures/plugins/newsfeed/server/plugin.ts index 85dadcfa8d7d2..5eb27325e535c 100644 --- a/test/common/fixtures/plugins/newsfeed/server/plugin.ts +++ b/test/common/fixtures/plugins/newsfeed/server/plugin.ts @@ -59,7 +59,7 @@ export class NewsFeedSimulatorPlugin implements Plugin { title: { en: 'Staging too!' }, description: { en: 'Hello world' }, link_text: { en: 'Generic feed-viewer could go here' }, - link_url: { en: 'https://feeds-staging.elastic.co' }, + link_url: { en: 'https://feeds.elastic.co' }, languages: null, badge: null, image_url: null, @@ -71,7 +71,7 @@ export class NewsFeedSimulatorPlugin implements Plugin { title: { en: 'This item is expired!' }, description: { en: 'This should not show up.' }, link_text: { en: 'Generic feed-viewer could go here' }, - link_url: { en: 'https://feeds-staging.elastic.co' }, + link_url: { en: 'https://feeds.elastic.co' }, languages: null, badge: null, image_url: null, diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 2d87c0575845f..d295be040db7a 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -139,7 +139,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'newsfeed.fetchInterval (duration)', 'newsfeed.mainInterval (duration)', 'newsfeed.service.pathTemplate (string)', - 'newsfeed.service.urlRoot (any)', + 'newsfeed.service.urlRoot (string)', 'telemetry.allowChangingOptInStatus (boolean)', 'telemetry.banner (boolean)', 'telemetry.enabled (boolean)', diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index b3dbe4801f544..ec855d98e7144 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -170,6 +170,10 @@ export const CasesFindRequestRt = rt.partial({ * The status of the case (open, closed, in-progress) */ status: CaseStatusRt, + /** + * The severity of the case + */ + severity: CaseSeverityRt, /** * The reporters to filter by */ diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 7ed9bfb3f2294..5443302bce467 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -20,6 +20,7 @@ import { CasesFindResponse, CasesStatusResponse, CasesMetricsResponse, + CaseSeverity, } from '../api'; import { SnakeToCamelCase } from '../types'; @@ -45,6 +46,9 @@ export type StatusAllType = typeof StatusAll; export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType; +export const SeverityAll = 'all' as const; +export type CaseSeverityWithAll = CaseSeverity | typeof SeverityAll; + /** * The type for the `refreshRef` prop (a `React.Ref`) defined by the `CaseViewComponentProps`. * @@ -84,6 +88,7 @@ export interface QueryParams { export interface FilterOptions { search: string; + severity: CaseSeverityWithAll; status: CaseStatusWithAllStatus; tags: string[]; reporters: User[]; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index f0a3502fd6813..22e12d5ee11b5 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -204,6 +204,11 @@ describe('AllCasesListGeneric', () => { .childAt(0) .prop('value') ).toBe(useGetCasesMockState.data.cases[0].createdAt); + + expect( + wrapper.find(`[data-test-subj="case-table-column-severity"]`).first().text().toLowerCase() + ).toBe(useGetCasesMockState.data.cases[0].severity); + expect(wrapper.find(`[data-test-subj="case-table-case-count"]`).first().text()).toEqual( 'Showing 10 cases' ); @@ -223,6 +228,7 @@ describe('AllCasesListGeneric', () => { createdAt: null, createdBy: null, status: null, + severity: null, tags: null, title: null, totalComment: null, diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 5eac485e24c7b..96b220283b452 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -218,6 +218,7 @@ export const AllCasesList = React.memo( tags: filterOptions.tags, status: filterOptions.status, owner: filterOptions.owner, + severity: filterOptions.severity, }} setFilterRefetch={setFilterRefetch} hiddenStatuses={hiddenStatuses} diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index 543e6ef6f4871..43096d3de061c 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -18,12 +18,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiHealth, } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { Case, DeleteCase } from '../../../common/ui/types'; -import { CaseStatuses, ActionConnector } from '../../../common/api'; +import { CaseStatuses, ActionConnector, CaseSeverity } from '../../../common/api'; import { OWNER_INFO } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; @@ -40,6 +41,7 @@ import { TruncatedText } from '../truncated_text'; import { getConnectorIcon } from '../utils'; import type { CasesOwners } from '../../client/helpers/can_use_cases'; import { useCasesFeatures } from '../cases_context/use_cases_features'; +import { severities } from '../severity/config'; export type CasesColumns = | EuiTableActionsColumnType @@ -300,30 +302,6 @@ export const useCasesColumns = ({ return getEmptyTagValue(); }, }, - ...(isSelectorView - ? [ - { - align: RIGHT_ALIGNMENT, - render: (theCase: Case) => { - if (theCase.id != null) { - return ( - { - assignCaseAction(theCase); - }} - size="s" - fill={true} - > - {i18n.SELECT} - - ); - } - return getEmptyTagValue(); - }, - }, - ] - : []), ...(!isSelectorView ? [ { @@ -351,6 +329,45 @@ export const useCasesColumns = ({ }, ] : []), + { + name: i18n.SEVERITY, + render: (theCase: Case) => { + if (theCase.severity != null) { + const severityData = severities[theCase.severity ?? CaseSeverity.LOW]; + return ( + + {severityData.label} + + ); + } + return getEmptyTagValue(); + }, + }, + + ...(isSelectorView + ? [ + { + align: RIGHT_ALIGNMENT, + render: (theCase: Case) => { + if (theCase.id != null) { + return ( + { + assignCaseAction(theCase); + }} + size="s" + fill={true} + > + {i18n.SELECT} + + ); + } + return getEmptyTagValue(); + }, + }, + ] + : []), ...(userCanCrud && !isSelectorView ? [ { diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.tsx new file mode 100644 index 0000000000000..7366bb3fceebb --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseSeverity } from '../../../common/api'; +import React from 'react'; +import { AppMockRenderer, createAppMockRenderer } from '../../common/mock'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; +import { SeverityFilter } from './severity_filter'; + +describe('Severity form field', () => { + const onSeverityChange = jest.fn(); + let appMockRender: AppMockRenderer; + const props = { + isLoading: false, + selectedSeverity: CaseSeverity.LOW, + isDisabled: false, + onSeverityChange, + }; + beforeEach(() => { + appMockRender = createAppMockRenderer(); + }); + it('renders', () => { + const result = appMockRender.render(); + expect(result.getByTestId('case-severity-filter')).not.toHaveAttribute('disabled'); + }); + + // default to LOW in this test configuration + it('defaults to the correct value', () => { + const result = appMockRender.render(); + // two items. one for the popover one for the selected field + expect(result.getAllByTestId('case-severity-filter-low').length).toBe(2); + }); + + it('selects the correct value when changed', async () => { + const result = appMockRender.render(); + userEvent.click(result.getByTestId('case-severity-filter')); + userEvent.click(result.getByTestId('case-severity-filter-high')); + await waitFor(() => { + expect(onSeverityChange).toHaveBeenCalledWith('high'); + }); + }); + + it('selects the correct value when changed (all)', async () => { + const result = appMockRender.render(); + userEvent.click(result.getByTestId('case-severity-filter')); + userEvent.click(result.getByTestId('case-severity-filter-all')); + await waitFor(() => { + expect(onSeverityChange).toHaveBeenCalledWith('all'); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx new file mode 100644 index 0000000000000..a9f4a6565c318 --- /dev/null +++ b/x-pack/plugins/cases/public/components/all_cases/severity_filter.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiSuperSelect, + EuiSuperSelectOption, + EuiText, +} from '@elastic/eui'; +import React from 'react'; +import { CaseSeverityWithAll, SeverityAll } from '../../containers/types'; +import { severitiesWithAll } from '../severity/config'; + +interface Props { + selectedSeverity: CaseSeverityWithAll; + onSeverityChange: (status: CaseSeverityWithAll) => void; + isLoading: boolean; + isDisabled: boolean; +} + +export const SeverityFilter: React.FC = ({ + selectedSeverity, + onSeverityChange, + isLoading, + isDisabled, +}) => { + const caseSeverities = Object.keys(severitiesWithAll) as CaseSeverityWithAll[]; + const options: Array> = caseSeverities.map( + (severity) => { + const severityData = severitiesWithAll[severity]; + return { + value: severity, + inputDisplay: ( + + + {severity === SeverityAll ? ( + {severityData.label} + ) : ( + {severityData.label} + )} + + + ), + }; + } + ); + + return ( + + ); +}; +SeverityFilter.displayName = 'SeverityFilter'; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index 5e83c33717abd..ff1c00b56d031 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -10,11 +10,12 @@ import { mount } from 'enzyme'; import { CaseStatuses } from '../../../common/api'; import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; -import { TestProviders } from '../../common/mock'; +import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; import { CasesTableFilters } from './table_filters'; +import userEvent from '@testing-library/user-event'; jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); @@ -35,7 +36,9 @@ const props = { }; describe('CasesTableFilters ', () => { + let appMockRender: AppMockRenderer; beforeEach(() => { + appMockRender = createAppMockRenderer(); jest.clearAllMocks(); (useGetTags as jest.Mock).mockReturnValue({ tags: ['coke', 'pepsi'], fetchTags }); (useGetReporters as jest.Mock).mockReturnValue({ @@ -57,6 +60,19 @@ describe('CasesTableFilters ', () => { expect(wrapper.find(`[data-test-subj="case-status-filter"]`).first().exists()).toBeTruthy(); }); + it('should render the case severity filter dropdown', () => { + const result = appMockRender.render(); + expect(result.getByTestId('case-severity-filter')).toBeTruthy(); + }); + + it('should call onFilterChange when the severity filter changes', () => { + const result = appMockRender.render(); + userEvent.click(result.getByTestId('case-severity-filter')); + userEvent.click(result.getByTestId('case-severity-filter-high')); + + expect(onFilterChanged).toBeCalledWith({ severity: 'high' }); + }); + it('should call onFilterChange when selected tags change', () => { const wrapper = mount( diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index faee469d1c4bc..0a34e756e37a6 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -10,7 +10,12 @@ import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup, EuiButton } from '@elastic/eui'; -import { StatusAll, CaseStatusWithAllStatus } from '../../../common/ui/types'; +import { + StatusAll, + CaseStatusWithAllStatus, + SeverityAll, + CaseSeverityWithAll, +} from '../../../common/ui/types'; import { CaseStatuses } from '../../../common/api'; import { FilterOptions } from '../../containers/types'; import { useGetTags } from '../../containers/use_get_tags'; @@ -18,6 +23,7 @@ import { useGetReporters } from '../../containers/use_get_reporters'; import { FilterPopover } from '../filter_popover'; import { StatusFilter } from './status_filter'; import * as i18n from './translations'; +import { SeverityFilter } from './severity_filter'; interface CasesTableFiltersProps { countClosedCases: number | null; @@ -39,6 +45,12 @@ const StatusFilterWrapper = styled(EuiFlexItem)` } `; +const SeverityFilterWrapper = styled(EuiFlexItem)` + && { + flex-basis: 180px; + } +`; + /** * Collection of filters for filtering data within the CasesTable. Contains search bar, * and tag selection @@ -48,6 +60,7 @@ const StatusFilterWrapper = styled(EuiFlexItem)` const defaultInitial = { search: '', + severity: SeverityAll, reporters: [], status: StatusAll, tags: [], @@ -151,6 +164,13 @@ const CasesTableFiltersComponent = ({ [onFilterChanged] ); + const onSeverityChanged = useCallback( + (severity: CaseSeverityWithAll) => { + onFilterChanged({ severity }); + }, + [onFilterChanged] + ); + const stats = useMemo( () => ({ [StatusAll]: null, @@ -181,6 +201,14 @@ const CasesTableFiltersComponent = ({ onSearch={handleOnSearch} /> + + + { }); }); + test('should apply the severity field correctly (with severity value)', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + severity: CaseSeverity.HIGH, + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + severity: CaseSeverity.HIGH, + }, + signal: abortCtrl.signal, + }); + }); + + test('should not send the severity field with "all" severity value', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + severity: 'all', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + }, + signal: abortCtrl.signal, + }); + }); + test('should handle tags with weird chars', async () => { const weirdTags: string[] = ['(', '"double"']; diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 63a2ea794e065..b0f00ad202c5f 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { omit } from 'lodash'; import { Cases, FetchCasesProps, ResolvedCase, + SeverityAll, SortFieldCase, StatusAll, } from '../../common/ui/types'; @@ -149,6 +149,7 @@ export const getCaseUserActions = async ( export const getCases = async ({ filterOptions = { search: '', + severity: SeverityAll, reporters: [], status: StatusAll, tags: [], @@ -163,9 +164,10 @@ export const getCases = async ({ signal, }: FetchCasesProps): Promise => { const query = { + ...(filterOptions.status !== StatusAll ? { status: filterOptions.status } : {}), + ...(filterOptions.severity !== SeverityAll ? { severity: filterOptions.severity } : {}), reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), tags: filterOptions.tags, - status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), ...(filterOptions.owner.length > 0 ? { owner: filterOptions.owner } : {}), ...queryParams, @@ -173,7 +175,7 @@ export const getCases = async ({ const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { method: 'GET', - query: query.status === StatusAll ? omit(query, ['status']) : query, + query, signal, }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index dee4d424c84de..b689746a7af00 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseStatuses } from '../../common/api'; +import { CaseSeverity, CaseStatuses } from '../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { DEFAULT_FILTER_OPTIONS, @@ -219,6 +219,7 @@ describe('useGetCases', () => { const spyOnGetCases = jest.spyOn(api, 'getCases'); const newFilters = { search: 'new', + severity: CaseSeverity.LOW, tags: ['new'], status: CaseStatuses.closed, owner: [SECURITY_SOLUTION_OWNER], diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx index d817dc9d9ac0f..f708d98282252 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -15,6 +15,7 @@ import { SortFieldCase, StatusAll, UpdateByKey, + SeverityAll, } from './types'; import { useToasts } from '../common/lib/kibana'; import * as i18n from './translations'; @@ -101,6 +102,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS export const DEFAULT_FILTER_OPTIONS: FilterOptions = { search: '', + severity: SeverityAll, reporters: [], status: StatusAll, tags: [], diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index b5d3cee05ced6..0c22229692842 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -53,6 +53,7 @@ export const find = async ( reporters: queryParams.reporters, sortByField: queryParams.sortField, status: queryParams.status, + severity: queryParams.severity, owner: queryParams.owner, from: queryParams.from, to: queryParams.to, diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index faae6450c5238..334b974c06108 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -23,6 +23,7 @@ import { ContextTypeUserRt, excess, throwErrors, + CaseSeverity, } from '../../common/api'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { @@ -114,6 +115,25 @@ export const addStatusFilter = ({ return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; }; +export const addSeverityFilter = ({ + severity, + appendFilter, + type = CASE_SAVED_OBJECT, +}: { + severity: CaseSeverity; + appendFilter?: KueryNode; + type?: string; +}): KueryNode => { + const filters: KueryNode[] = []; + filters.push(nodeBuilder.is(`${type}.attributes.severity`, severity)); + + if (appendFilter) { + filters.push(appendFilter); + } + + return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; +}; + interface FilterField { filters?: string | string[]; field: string; @@ -222,6 +242,7 @@ export const constructQueryOptions = ({ tags, reporters, status, + severity, sortByField, owner, authorizationFilter, @@ -231,6 +252,7 @@ export const constructQueryOptions = ({ tags?: string | string[]; reporters?: string | string[]; status?: CaseStatuses; + severity?: CaseSeverity; sortByField?: string; owner?: string | string[]; authorizationFilter?: KueryNode; @@ -250,10 +272,12 @@ export const constructQueryOptions = ({ const ownerFilter = buildFilter({ filters: owner ?? [], field: OWNER_FIELD, operator: 'or' }); const statusFilter = status != null ? addStatusFilter({ status }) : undefined; + const severityFilter = severity != null ? addSeverityFilter({ severity }) : undefined; const rangeFilter = buildRangeFilter({ from, to }); const filters: KueryNode[] = [ statusFilter, + severityFilter, tagsFilter, reportersFilter, rangeFilter, diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 018f591fef79c..68c2a0ecb7d88 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -104,6 +104,7 @@ interface AgentBase { last_checkin_status?: 'error' | 'online' | 'degraded' | 'updating'; user_provided_metadata: AgentMetadata; local_metadata: AgentMetadata; + tags?: string[]; } export interface Agent extends AgentBase { @@ -216,6 +217,10 @@ export interface FleetServerAgent { * The last acknowledged action sequence number for the Elastic Agent */ action_seq_no?: number; + /** + * A list of tags used for organizing/filtering agents + */ + tags?: string[]; } /** * An Elastic Agent metadata diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx index 87b4a1bda7ff7..5c5f87b19f977 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/advanced_tab.tsx @@ -57,6 +57,7 @@ export const AdvancedTab: React.FunctionComponent = () => { serviceToken, fleetServerHost: fleetServerHostForm.fleetServerHost, fleetServerPolicyId, + deploymentMode, disabled: !Boolean(serviceToken), }), getConfirmFleetServerConnectionStep({ isFleetServerReady, disabled: !Boolean(serviceToken) }), diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/quick_start_tab.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/quick_start_tab.tsx index cf8abc2fe9e16..758a34113efcd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/quick_start_tab.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/quick_start_tab.tsx @@ -29,6 +29,7 @@ export const QuickStartTab: React.FunctionComponent = () => { fleetServerHost: quickStartCreateForm.fleetServerHost, fleetServerPolicyId: quickStartCreateForm.fleetServerPolicyId, serviceToken: quickStartCreateForm.serviceToken, + deploymentMode: 'quickstart', disabled: quickStartCreateForm.status !== 'success', }), getConfirmFleetServerConnectionStep({ diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/add_fleet_server_host.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/add_fleet_server_host.tsx index e64e23f039f89..70753e37f8e8a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/add_fleet_server_host.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/add_fleet_server_host.tsx @@ -104,8 +104,13 @@ export const AddFleetServerHostStepContent = ({ /> - - + + ), }; @@ -51,7 +56,8 @@ const InstallFleetServerStepContent: React.FunctionComponent<{ serviceToken?: string; fleetServerHost?: string; fleetServerPolicyId?: string; -}> = ({ serviceToken, fleetServerHost, fleetServerPolicyId }) => { + deploymentMode: DeploymentMode; +}> = ({ serviceToken, fleetServerHost, fleetServerPolicyId, deploymentMode }) => { const kibanaVersion = useKibanaVersion(); const { output } = useDefaultOutput(); @@ -63,7 +69,7 @@ const InstallFleetServerStepContent: React.FunctionComponent<{ serviceToken ?? '', fleetServerPolicyId, fleetServerHost, - false, + deploymentMode === 'production', output?.ca_trusted_fingerprint, kibanaVersion ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts index 89a246c5c6265..12c1af65f9555 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.test.ts @@ -17,9 +17,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip \\\\ - tar xzvf elastic-agent--linux-x86_64.zip \\\\ - cd elastic-agent--linux-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip + tar xzvf elastic-agent--linux-x86_64.zip + cd elastic-agent--linux-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" @@ -34,9 +34,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--darwin-x86_64.tar.gz \\\\ - tar xzvf elastic-agent--darwin-x86_64.tar.gz \\\\ - cd elastic-agent--darwin-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--darwin-x86_64.tar.gz + tar xzvf elastic-agent--darwin-x86_64.tar.gz + cd elastic-agent--darwin-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" @@ -51,9 +51,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz \` - Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz \` - cd elastic-agent--windows-x86_64\` + "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz + Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz + cd elastic-agent--windows-x86_64 .\\\\elastic-agent.exe install \` --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1" @@ -68,9 +68,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm \\\\ - tar xzvf elastic-agent--x86_64.rpm \\\\ - cd elastic-agent--x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm + tar xzvf elastic-agent--x86_64.rpm + cd elastic-agent--x86_64 sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" @@ -85,9 +85,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb \\\\ - tar xzvf elastic-agent--amd64.deb \\\\ - cd elastic-agent--amd64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb + tar xzvf elastic-agent--amd64.deb + cd elastic-agent--amd64 sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1" @@ -106,9 +106,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip \\\\ - tar xzvf elastic-agent--linux-x86_64.zip \\\\ - cd elastic-agent--linux-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip + tar xzvf elastic-agent--linux-x86_64.zip + cd elastic-agent--linux-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -127,9 +127,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip \\\\ - tar xzvf elastic-agent--linux-x86_64.zip \\\\ - cd elastic-agent--linux-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip + tar xzvf elastic-agent--linux-x86_64.zip + cd elastic-agent--linux-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -146,9 +146,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--darwin-x86_64.tar.gz \\\\ - tar xzvf elastic-agent--darwin-x86_64.tar.gz \\\\ - cd elastic-agent--darwin-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--darwin-x86_64.tar.gz + tar xzvf elastic-agent--darwin-x86_64.tar.gz + cd elastic-agent--darwin-x86_64 sudo ./elastic-agent install \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -165,9 +165,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz \` - Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz \` - cd elastic-agent--windows-x86_64\` + "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz + Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz + cd elastic-agent--windows-x86_64 .\\\\elastic-agent.exe install \` --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1 \` @@ -184,9 +184,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm \\\\ - tar xzvf elastic-agent--x86_64.rpm \\\\ - cd elastic-agent--x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm + tar xzvf elastic-agent--x86_64.rpm + cd elastic-agent--x86_64 sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -203,9 +203,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb \\\\ - tar xzvf elastic-agent--amd64.deb \\\\ - cd elastic-agent--amd64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb + tar xzvf elastic-agent--amd64.deb + cd elastic-agent--amd64 sudo elastic-agent enroll \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -226,9 +226,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip \\\\ - tar xzvf elastic-agent--linux-x86_64.zip \\\\ - cd elastic-agent--linux-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--linux-x86_64.zip + tar xzvf elastic-agent--linux-x86_64.zip + cd elastic-agent--linux-x86_64 sudo ./elastic-agent install--url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -251,9 +251,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--darwin-x86_64.tar.gz \\\\ - tar xzvf elastic-agent--darwin-x86_64.tar.gz \\\\ - cd elastic-agent--darwin-x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--darwin-x86_64.tar.gz + tar xzvf elastic-agent--darwin-x86_64.tar.gz + cd elastic-agent--darwin-x86_64 sudo ./elastic-agent install --url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -276,9 +276,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz \` - Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz \` - cd elastic-agent--windows-x86_64\` + "wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--windows-x86_64.tar.gz -OutFile elastic-agent--windows-x86_64.tar.gz + Expand-Archive .\\\\elastic-agent--windows-x86_64.tar.gz + cd elastic-agent--windows-x86_64 .\\\\elastic-agent.exe install --url=http://fleetserver:8220 \` --fleet-server-es=http://elasticsearch:9200 \` --fleet-server-service-token=service-token-1 \` @@ -301,9 +301,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm \\\\ - tar xzvf elastic-agent--x86_64.rpm \\\\ - cd elastic-agent--x86_64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--x86_64.rpm + tar xzvf elastic-agent--x86_64.rpm + cd elastic-agent--x86_64 sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ @@ -326,9 +326,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb \\\\ - tar xzvf elastic-agent--amd64.deb \\\\ - cd elastic-agent--amd64 \\\\ + "curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb + tar xzvf elastic-agent--amd64.deb + cd elastic-agent--amd64 sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ --fleet-server-es=http://elasticsearch:9200 \\\\ --fleet-server-service-token=service-token-1 \\\\ diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts index 525af7cf95103..ed38478c3a3ee 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/utils/install_command_utils.ts @@ -67,12 +67,12 @@ export function getInstallCommandForPlatform( `wget ${artifact.fullUrl} -OutFile ${artifact.filename}`, `Expand-Archive .\\${artifact.filename}`, `cd ${artifact.unpackedDir}`, - ].join(` ${newLineSeparator}`) + ].join(`\n`) : [ `curl -L -O ${artifact.fullUrl}`, `tar xzvf ${artifact.filename}`, `cd ${artifact.unpackedDir}`, - ].join(` ${newLineSeparator}`); + ].join(`\n`); const commandArguments = []; @@ -108,11 +108,11 @@ export function getInstallCommandForPlatform( }, ''); const commands = { - linux: `${downloadCommand} ${newLineSeparator}sudo ./elastic-agent install${commandArgumentsStr}`, - mac: `${downloadCommand} ${newLineSeparator}sudo ./elastic-agent install ${commandArgumentsStr}`, - windows: `${downloadCommand}${newLineSeparator}.\\elastic-agent.exe install ${commandArgumentsStr}`, - deb: `${downloadCommand} ${newLineSeparator}sudo elastic-agent enroll ${commandArgumentsStr}`, - rpm: `${downloadCommand} ${newLineSeparator}sudo elastic-agent enroll ${commandArgumentsStr}`, + linux: `${downloadCommand}\nsudo ./elastic-agent install${commandArgumentsStr}`, + mac: `${downloadCommand}\nsudo ./elastic-agent install ${commandArgumentsStr}`, + windows: `${downloadCommand}\n.\\elastic-agent.exe install ${commandArgumentsStr}`, + deb: `${downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}`, + rpm: `${downloadCommand}\nsudo elastic-agent enroll ${commandArgumentsStr}`, }; return commands[platform]; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index a511c2dc9f3da..60a97845312e8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -70,6 +70,9 @@ export const SearchAndFilterBar: React.FunctionComponent<{ onSelectedStatusChange: (selectedStatus: string[]) => void; showUpgradeable: boolean; onShowUpgradeableChange: (showUpgradeable: boolean) => void; + tags: string[]; + selectedTags: string[]; + onSelectedTagsChange: (selectedTags: string[]) => void; totalAgents: number; totalInactiveAgents: number; selectionMode: SelectionMode; @@ -87,6 +90,9 @@ export const SearchAndFilterBar: React.FunctionComponent<{ onSelectedStatusChange, showUpgradeable, onShowUpgradeableChange, + tags, + selectedTags, + onSelectedTagsChange, totalAgents, totalInactiveAgents, selectionMode, @@ -100,7 +106,9 @@ export const SearchAndFilterBar: React.FunctionComponent<{ const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); // Status for filtering - const [isStatusFilterOpen, setIsStatutsFilterOpen] = useState(false); + const [isStatusFilterOpen, setIsStatusFilterOpen] = useState(false); + + const [isTagsFilterOpen, setIsTagsFilterOpen] = useState(false); // Add a agent policy id to current search const addAgentPolicyFilter = (policyId: string) => { @@ -114,6 +122,14 @@ export const SearchAndFilterBar: React.FunctionComponent<{ ); }; + const addTagsFilter = (tag: string) => { + onSelectedTagsChange([...selectedTags, tag]); + }; + + const removeTagsFilter = (tag: string) => { + onSelectedTagsChange(selectedTags.filter((t) => t !== tag)); + }; + return ( <> {isEnrollmentFlyoutOpen ? ( @@ -146,7 +162,7 @@ export const SearchAndFilterBar: React.FunctionComponent<{ button={ setIsStatutsFilterOpen(!isStatusFilterOpen)} + onClick={() => setIsStatusFilterOpen(!isStatusFilterOpen)} isSelected={isStatusFilterOpen} hasActiveFilters={selectedStatus.length > 0} disabled={agentPolicies.length === 0} @@ -159,7 +175,7 @@ export const SearchAndFilterBar: React.FunctionComponent<{ } isOpen={isStatusFilterOpen} - closePopover={() => setIsStatutsFilterOpen(false)} + closePopover={() => setIsStatusFilterOpen(false)} panelPaddingSize="none" >
@@ -180,6 +196,46 @@ export const SearchAndFilterBar: React.FunctionComponent<{ ))}
+ setIsTagsFilterOpen(!isTagsFilterOpen)} + isSelected={isTagsFilterOpen} + hasActiveFilters={selectedTags.length > 0} + numFilters={selectedTags.length} + disabled={tags.length === 0} + data-test-subj="agentList.tagsFilter" + > + + + } + isOpen={isTagsFilterOpen} + closePopover={() => setIsTagsFilterOpen(false)} + panelPaddingSize="none" + > +
+ {tags.map((tag, index) => ( + { + if (selectedTags.includes(tag)) { + removeTagsFilter(tag); + } else { + addTagsFilter(tag); + } + }} + > + {tag} + + ))} +
+
{ + describe('when list is short', () => { + it('renders a comma-separated list of tags', () => { + const tags = ['tag1', 'tag2']; + render(); + + expect(screen.getByTestId('agentTags')).toHaveTextContent('tag1, tag2'); + }); + }); + + describe('when list is long', () => { + it('renders a truncated list of tags with full list displayed in tooltip on hover', async () => { + const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5']; + render(); + + const tagsNode = screen.getByTestId('agentTags'); + + expect(tagsNode).toHaveTextContent('tag1, tag2, tag3 + 2 more'); + + fireEvent.mouseEnter(tagsNode); + await waitFor(() => { + screen.getByTestId('agentTagsTooltip'); + }); + + expect(screen.getByTestId('agentTagsTooltip')).toHaveTextContent( + 'tag1, tag2, tag3, tag4, tag5' + ); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx new file mode 100644 index 0000000000000..7650b0d942180 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiToolTip } from '@elastic/eui'; +import { take } from 'lodash'; +import React from 'react'; + +interface Props { + tags: string[]; +} + +const MAX_TAGS_TO_DISPLAY = 3; + +export const Tags: React.FunctionComponent = ({ tags }) => { + return ( + <> + {tags.length > MAX_TAGS_TO_DISPLAY ? ( + <> + {tags.join(', ')}}> + + {take(tags, 3).join(', ')} + {tags.length - MAX_TAGS_TO_DISPLAY} more + + + + ) : ( + {tags.join(', ')} + )} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 5776a163fd6a3..510be94ab1705 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -50,6 +50,7 @@ import { agentFlyoutContext } from '..'; import { AgentTableHeader } from './components/table_header'; import type { SelectionMode } from './components/types'; import { SearchAndFilterBar } from './components/search_and_filter_bar'; +import { Tags } from './components/tags'; import { TableRowActions } from './components/table_row_actions'; import { EmptyPrompt } from './components/empty_prompt'; @@ -98,14 +99,21 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Status for filtering const [selectedStatus, setSelectedStatus] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); + const isUsingFilter = - search.trim() || selectedAgentPolicies.length || selectedStatus.length || showUpgradeable; + search.trim() || + selectedAgentPolicies.length || + selectedStatus.length || + selectedTags.length || + showUpgradeable; const clearFilters = useCallback(() => { setDraftKuery(''); setSearch(''); setSelectedAgentPolicies([]); setSelectedStatus([]); + setSelectedTags([]); setShowUpgradeable(false); }, [setSearch, setDraftKuery, setSelectedAgentPolicies, setSelectedStatus, setShowUpgradeable]); @@ -135,6 +143,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { .map((agentPolicy) => `"${agentPolicy}"`) .join(' or ')})`; } + + if (selectedTags.length) { + kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.tags : (${selectedTags + .map((tag) => `"${tag}"`) + .join(' or ')})`; + } + if (selectedStatus.length) { const kueryStatus = selectedStatus .map((status) => { @@ -164,7 +179,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { } return kueryBuilder; - }, [selectedStatus, selectedAgentPolicies, search]); + }, [search, selectedAgentPolicies, selectedTags, selectedStatus]); const showInactive = useMemo(() => { return selectedStatus.includes('inactive'); @@ -174,6 +189,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [agentsStatus, setAgentsStatus] = useState< { [key in SimplifiedAgentStatus]: number } | undefined >(); + const [allTags, setAllTags] = useState(); const [isLoading, setIsLoading] = useState(false); const [totalAgents, setTotalAgents] = useState(0); const [totalInactiveAgents, setTotalInactiveAgents] = useState(0); @@ -224,6 +240,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { inactive: agentsRequest.data.totalInactive, }); + // Only set tags on the first request - we don't want the list of tags to update based + // on the returned set of agents from the API + if (allTags === undefined) { + const newAllTags = Array.from( + new Set(agentsRequest.data.items.flatMap((agent) => agent.tags ?? [])) + ); + + setAllTags(newAllTags); + } + setAgents(agentsRequest.data.items); setTotalAgents(agentsRequest.data.total); setTotalInactiveAgents(agentsRequest.data.totalInactive); @@ -237,7 +263,15 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setIsLoading(false); } fetchDataAsync(); - }, [pagination, kuery, showInactive, showUpgradeable, notifications.toasts]); + }, [ + pagination.currentPage, + pagination.pageSize, + kuery, + showInactive, + showUpgradeable, + allTags, + notifications.toasts, + ]); // Send request to get agent list and status useEffect(() => { @@ -319,6 +353,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }), render: (active: boolean, agent: any) => , }, + { + field: 'tags', + width: '240px', + name: i18n.translate('xpack.fleet.agentList.tagsColumnTitle', { + defaultMessage: 'Tags', + }), + render: (tags: string[] = [], agent: any) => , + }, { field: 'policy_id', name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', { @@ -481,6 +523,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onSelectedStatusChange={setSelectedStatus} showUpgradeable={showUpgradeable} onShowUpgradeableChange={setShowUpgradeable} + tags={allTags ?? []} + selectedTags={selectedTags} + onSelectedTagsChange={setSelectedTags} totalAgents={totalAgents} totalInactiveAgents={totalInactiveAgents} selectionMode={selectionMode} diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index 5b777803552fb..ca7293a8c99c9 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -26,8 +26,9 @@ tar xzvf elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz cd elastic-agent-${kibanaVersion}-darwin-x86_64 sudo ./elastic-agent install ${enrollArgs}`; - const windowsCommand = `wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip -Expand-Archive .\\elastic-agent-${kibanaVersion}-windows-x86_64.zip + const windowsCommand = `$ProgressPreference = 'SilentlyContinue' +wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip +Expand-Archive .\\elastic-agent-${kibanaVersion}-windows-x86_64.zip -DestinationPath . cd elastic-agent-${kibanaVersion}-windows-x86_64 .\\elastic-agent.exe install ${enrollArgs}`; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx index 75378cdc86378..2d9326cf6cbb1 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx @@ -23,8 +23,9 @@ tar xzvf elastic-agent-${kibanaVersion}-darwin-x86_64.tar.gz cd elastic-agent-${kibanaVersion}-darwin-x86_64 sudo ./elastic-agent install`; - const STANDALONE_RUN_INSTRUCTIONS_WINDOWS = `wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip -Expand-Archive .\elastic-agent-${kibanaVersion}-windows-x86_64.zip + const STANDALONE_RUN_INSTRUCTIONS_WINDOWS = `$ProgressPreference = 'SilentlyContinue' +wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip +Expand-Archive .\elastic-agent-${kibanaVersion}-windows-x86_64.zip -DestinationPath . cd elastic-agent-${kibanaVersion}-windows-x86_64 .\\elastic-agent.exe install`; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index e5a55322a2f10..adf791e8d2f48 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -16,6 +16,7 @@ "visualizations", "dashboard", "uiActions", + "uiActionsEnhanced", "embeddable", "share", "presentationUtil", diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 876cb63b0333d..e3c879d864a46 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -44,6 +44,7 @@ import { VISUALIZE_EDITOR_TRIGGER } from '@kbn/visualizations-plugin/public'; import { createStartServicesGetter } from '@kbn/kibana-utils-plugin/public'; import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { AdvancedUiActionsSetup } from '@kbn/ui-actions-enhanced-plugin/public'; import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; import type { IndexPatternDatasource as IndexPatternDatasourceType, @@ -93,6 +94,7 @@ import type { SaveModalContainerProps } from './app_plugin/save_modal_container' import { setupExpressions } from './expressions'; import { getSearchProvider } from './search_provider'; +import { OpenInDiscoverDrilldown } from './trigger_actions/open_in_discover_drilldown'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -106,6 +108,7 @@ export interface LensPluginSetupDependencies { globalSearch?: GlobalSearchPluginSetup; usageCollection?: UsageCollectionSetup; discover?: DiscoverSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; } export interface LensPluginStartDependencies { @@ -224,6 +227,7 @@ export class LensPlugin { private heatmapVisualization: HeatmapVisualizationType | undefined; private gaugeVisualization: GaugeVisualizationType | undefined; private topNavMenuEntries: LensTopNavMenuEntryGenerator[] = []; + private hasDiscoverAccess: boolean = false; private stopReportManager?: () => void; @@ -240,6 +244,8 @@ export class LensPlugin { eventAnnotation, globalSearch, usageCollection, + uiActionsEnhanced, + discover, }: LensPluginSetupDependencies ) { const startServices = createStartServicesGetter(core.getStartServices); @@ -285,6 +291,15 @@ export class LensPlugin { visualizations.registerAlias(getLensAliasConfig()); + if (discover) { + uiActionsEnhanced.registerDrilldown( + new OpenInDiscoverDrilldown({ + discover, + hasDiscoverAccess: () => this.hasDiscoverAccess, + }) + ); + } + setupExpressions( expressions, () => startServices().plugins.fieldFormats.deserialize, @@ -427,6 +442,7 @@ export class LensPlugin { } start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { + this.hasDiscoverAccess = core.application.capabilities.discover.show as boolean; // unregisters the Visualize action and registers the lens one if (startDependencies.uiActions.hasAction(ACTION_VISUALIZE_FIELD)) { startDependencies.uiActions.unregisterAction(ACTION_VISUALIZE_FIELD); @@ -443,10 +459,7 @@ export class LensPlugin { startDependencies.uiActions.addTriggerAction( CONTEXT_MENU_TRIGGER, - createOpenInDiscoverAction( - startDependencies.discover!, - core.application.capabilities.discover.show as boolean - ) + createOpenInDiscoverAction(startDependencies.discover!, this.hasDiscoverAccess) ); return { diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts index 084bd65b70d31..eebdf04337f69 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts @@ -83,6 +83,7 @@ describe('open in discover action', () => { const embeddable = { getViewUnderlyingDataArgs: jest.fn(() => viewUnderlyingDataArgs), + type: 'lens', }; const discoverUrl = 'https://discover-redirect-url'; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index bd666f52bf0bc..54a24aac269b5 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -5,17 +5,23 @@ * 2.0. */ -import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { createAction } from '@kbn/ui-actions-plugin/public'; import type { DiscoverStart } from '@kbn/discover-plugin/public'; -import type { Embeddable } from '../embeddable'; -import { DOC_TYPE } from '../../common'; +import { IEmbeddable } from '@kbn/embeddable-plugin/public'; +import { execute, isCompatible } from './open_in_discover_helpers'; const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER'; -export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverAccess: boolean) => - createAction<{ embeddable: IEmbeddable }>({ +interface Context { + embeddable: IEmbeddable; +} + +export const createOpenInDiscoverAction = ( + discover: Pick, + hasDiscoverAccess: boolean +) => + createAction({ type: ACTION_OPEN_IN_DISCOVER, id: ACTION_OPEN_IN_DISCOVER, order: 19, // right after Inspect which is 20 @@ -24,18 +30,10 @@ export const createOpenInDiscoverAction = (discover: DiscoverStart, hasDiscoverA i18n.translate('xpack.lens.app.exploreDataInDiscover', { defaultMessage: 'Explore data in Discover', }), - isCompatible: async (context: { embeddable: IEmbeddable }) => { - if (!hasDiscoverAccess) return false; - return ( - context.embeddable.type === DOC_TYPE && - (await (context.embeddable as Embeddable).canViewUnderlyingData()) - ); + isCompatible: async (context: Context) => { + return isCompatible({ hasDiscoverAccess, discover, embeddable: context.embeddable }); }, - execute: async (context: { embeddable: Embeddable }) => { - const args = context.embeddable.getViewUnderlyingDataArgs()!; - const discoverUrl = discover.locator?.getRedirectUrl({ - ...args, - }); - window.open(discoverUrl, '_blank'); + execute: async (context: Context) => { + return execute({ ...context, discover, hasDiscoverAccess }); }, }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx new file mode 100644 index 0000000000000..bd1fc948eb937 --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FormEvent } from 'react'; +import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { DiscoverSetup } from '@kbn/discover-plugin/public'; +import { execute, isCompatible } from './open_in_discover_helpers'; +import { mount } from 'enzyme'; +import { Filter } from '@kbn/es-query'; +import { + ActionFactoryContext, + CollectConfigProps, + OpenInDiscoverDrilldown, +} from './open_in_discover_drilldown'; + +jest.mock('./open_in_discover_helpers', () => ({ + isCompatible: jest.fn(() => true), + execute: jest.fn(), +})); + +describe('open in discover drilldown', () => { + let drilldown: OpenInDiscoverDrilldown; + beforeEach(() => { + drilldown = new OpenInDiscoverDrilldown({ + discover: {} as DiscoverSetup, + hasDiscoverAccess: () => true, + }); + }); + it('provides UI to edit config', () => { + const Component = (drilldown as unknown as { ReactCollectConfig: React.FC }) + .ReactCollectConfig; + const setConfig = jest.fn(); + const instance = mount( + + ); + instance.find('EuiSwitch').prop('onChange')!({} as unknown as FormEvent<{}>); + expect(setConfig).toHaveBeenCalledWith({ openInNewTab: true }); + }); + it('calls through to isCompatible helper', () => { + const filters: Filter[] = [{ meta: { disabled: false } }]; + drilldown.isCompatible( + { openInNewTab: true }, + { embeddable: { type: 'lens' } as IEmbeddable, filters } + ); + expect(isCompatible).toHaveBeenCalledWith(expect.objectContaining({ filters })); + }); + it('calls through to execute helper', () => { + const filters: Filter[] = [{ meta: { disabled: false } }]; + drilldown.execute( + { openInNewTab: true }, + { embeddable: { type: 'lens' } as IEmbeddable, filters } + ); + expect(execute).toHaveBeenCalledWith( + expect.objectContaining({ filters, openInSameTab: false }) + ); + }); +}); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx new file mode 100644 index 0000000000000..d957b9cafd4be --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_drilldown.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { IEmbeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { + Query, + Filter, + TimeRange, + extractTimeRange, + APPLY_FILTER_TRIGGER, +} from '@kbn/data-plugin/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '@kbn/kibana-utils-plugin/public'; +import { reactToUiComponent } from '@kbn/kibana-react-plugin/public'; +import { + UiActionsEnhancedDrilldownDefinition as Drilldown, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, +} from '@kbn/ui-actions-enhanced-plugin/public'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { DiscoverSetup } from '@kbn/discover-plugin/public'; +import { ApplyGlobalFilterActionContext } from '@kbn/unified-search-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { execute, isCompatible, isLensEmbeddable } from './open_in_discover_helpers'; + +interface EmbeddableQueryInput extends EmbeddableInput { + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; +} + +/** @internal */ +export type EmbeddableWithQueryInput = IEmbeddable; + +interface UrlDrilldownDeps { + discover: Pick; + hasDiscoverAccess: () => boolean; +} + +export type ActionContext = ApplyGlobalFilterActionContext; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type Config = { + openInNewTab: boolean; +}; + +export type OpenInDiscoverTrigger = typeof APPLY_FILTER_TRIGGER; + +export interface ActionFactoryContext extends BaseActionFactoryContext { + embeddable?: EmbeddableWithQueryInput; +} +export type CollectConfigProps = CollectConfigPropsBase; + +const OPEN_IN_DISCOVER_DRILLDOWN = 'OPEN_IN_DISCOVER_DRILLDOWN'; + +export class OpenInDiscoverDrilldown + implements Drilldown +{ + public readonly id = OPEN_IN_DISCOVER_DRILLDOWN; + + constructor(private readonly deps: UrlDrilldownDeps) {} + + public readonly order = 8; + + public readonly getDisplayName = () => + i18n.translate('xpack.lens.app.exploreDataInDiscoverDrilldown', { + defaultMessage: 'Open in Discover', + }); + + public readonly euiIcon = 'discoverApp'; + + supportedTriggers(): OpenInDiscoverTrigger[] { + return [APPLY_FILTER_TRIGGER]; + } + + private readonly ReactCollectConfig: React.FC = ({ + config, + onConfig, + context, + }) => { + return ( + + onConfig({ ...config, openInNewTab: !config.openInNewTab })} + data-test-subj="openInDiscoverDrilldownOpenInNewTab" + /> + + ); + }; + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + openInNewTab: true, + }); + + public readonly isConfigValid = (config: Config): config is Config => { + return true; + }; + + public readonly isCompatible = async (config: Config, context: ActionContext) => { + return isCompatible({ + discover: this.deps.discover, + hasDiscoverAccess: this.deps.hasDiscoverAccess(), + ...context, + embeddable: context.embeddable as IEmbeddable, + ...config, + }); + }; + + public readonly isConfigurable = (context: ActionFactoryContext) => { + return this.deps.hasDiscoverAccess() && isLensEmbeddable(context.embeddable as IEmbeddable); + }; + + public readonly execute = async (config: Config, context: ActionContext) => { + const { restOfFilters: filters, timeRange: timeRange } = extractTimeRange( + context.filters, + context.timeFieldName + ); + execute({ + discover: this.deps.discover, + hasDiscoverAccess: this.deps.hasDiscoverAccess(), + ...context, + embeddable: context.embeddable as IEmbeddable, + openInSameTab: !config.openInNewTab, + filters, + timeRange, + }); + }; +} diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts new file mode 100644 index 0000000000000..87f0931f1a3db --- /dev/null +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DiscoverSetup } from '@kbn/discover-plugin/public'; +import { Filter } from '@kbn/es-query'; +import { TimeRange } from '@kbn/data-plugin/public'; +import { IEmbeddable } from '@kbn/embeddable-plugin/public'; +import type { Embeddable } from '../embeddable'; +import { DOC_TYPE } from '../../common'; + +interface Context { + embeddable: IEmbeddable; + filters?: Filter[]; + timeRange?: TimeRange; + openInSameTab?: boolean; + hasDiscoverAccess: boolean; + discover: Pick; +} + +export function isLensEmbeddable(embeddable: IEmbeddable): embeddable is Embeddable { + return embeddable.type === DOC_TYPE; +} + +export async function isCompatible({ hasDiscoverAccess, embeddable }: Context) { + if (!hasDiscoverAccess) return false; + return isLensEmbeddable(embeddable) && (await embeddable.canViewUnderlyingData()); +} + +export function execute({ embeddable, discover, timeRange, filters, openInSameTab }: Context) { + if (!isLensEmbeddable(embeddable)) { + // shouldn't be executed because of the isCompatible check + throw new Error('Can only be executed in the context of Lens visualization'); + } + const args = embeddable.getViewUnderlyingDataArgs(); + if (!args) { + // shouldn't be executed because of the isCompatible check + throw new Error('Underlying data is not ready'); + } + const discoverUrl = discover.locator?.getRedirectUrl({ + ...args, + timeRange: timeRange || args.timeRange, + filters: [...(filters || []), ...args.filters], + }); + window.open(discoverUrl, !openInSameTab ? '_blank' : '_self'); +} diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index e00581833f621..20def97df7aed 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -12,6 +12,7 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../global_search/tsconfig.json" }, + { "path": "../ui_actions_enhanced/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/data_views/tsconfig.json" }, diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 4c4ca64f7ac07..edbf4df979f7b 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -25,7 +25,8 @@ "mapsEms", "savedObjects", "share", - "presentationUtil" + "presentationUtil", + "screenshotMode" ], "optionalPlugins": [ "cloud", diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 5e9662c543641..b5b232aeeaae6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -41,6 +41,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { LensPublicSetup } from '@kbn/lens-plugin/public'; +import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; import { createRegionMapFn, regionMapRenderer, @@ -88,6 +89,7 @@ export interface MapsPluginSetupDependencies { share: SharePluginSetup; licensing: LicensingPluginSetup; usageCollection?: UsageCollectionSetup; + screenshotMode: ScreenshotModePluginSetup; } export interface MapsPluginStartDependencies { @@ -144,7 +146,15 @@ export class MapsPlugin registerLicensedFeatures(plugins.licensing); const config = this._initializerContext.config.get(); - setMapAppConfig(config); + setMapAppConfig({ + ...config, + + // Override this when we know we are taking a screenshot (i.e. no user interaction) + // to avoid a blank-canvas issue when rendering maps on a PDF + preserveDrawingBuffer: plugins.screenshotMode.isScreenshotMode() + ? true + : config.preserveDrawingBuffer, + }); const locator = plugins.share.url.locators.create( new MapsAppLocatorDefinition({ diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 5d5f4223fab9a..57cc09dec4b16 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -33,6 +33,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/shared_ux/tsconfig.json" }, + { "path": "../../../src/plugins/screenshot_mode/tsconfig.json" }, { "path": "../cloud/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, diff --git a/x-pack/plugins/ml/readme.md b/x-pack/plugins/ml/readme.md index 7bd1a1a221edd..9b155e5f7696c 100644 --- a/x-pack/plugins/ml/readme.md +++ b/x-pack/plugins/ml/readme.md @@ -104,42 +104,49 @@ Run the following commands from the `x-pack` directory and use separate terminal for test server and test runner. The test server command starts an Elasticsearch and Kibana instance that the tests will be run against. -1. Functional UI tests with `Trial` license (default config): - - node scripts/functional_tests_server.js - node scripts/functional_test_runner.js --include-tag mlqa - - ML functional `Trial` license tests are located in `x-pack/test/functional/apps/ml`. - +Functional tests are broken up into independent groups with their own configuration. +Test server and runner need to be pointed to the configuration to run. The basic +commands are + + node scripts/functional_tests_server.js --config PATH_TO_CONFIG + node scripts/functional_test_runner.js --config PATH_TO_CONFIG + +With PATH_TO_CONFIG and other options as follows. + +1. Functional UI tests with `Trial` license: + + Group | PATH_TO_CONFIG + ----- | -------------- + anomaly detection | `test/functional/apps/ml/anomaly_detection/config.ts` + data frame analytics | `test/functional/apps/ml/anomaly_detection/config.ts` + data visualizer | `test/functional/apps/ml/data_frame_analytics/config.ts` + permissions | `test/functional/apps/ml/permissions/config.ts` + stack management jobs | `test/functional/apps/ml/stack_management_jobs/config.ts` + short tests | `test/functional/apps/ml/short_tests/config.ts` + + The `short tests` group contains tests for page navigation, model management, + feature controls, settings and embeddables. Test files for each group are located + in the directory of their copnfiguration file. + 1. Functional UI tests with `Basic` license: - node scripts/functional_tests_server.js --config test/functional_basic/config.ts - node scripts/functional_test_runner.js --config test/functional_basic/config.ts --include-tag mlqa - - ML functional `Basic` license tests are located in `x-pack/test/functional_basic/apps/ml`. + - PATH_TO_CONFIG: `test/functional_basic/config.ts` + - Add `--include-tag ml` to the test runner command + - Tests are located in `x-pack/test/functional_basic/apps/ml` 1. API integration tests with `Trial` license: - node scripts/functional_tests_server.js - node scripts/functional_test_runner.js --config test/api_integration/config.ts --include-tag mlqa - - ML API integration `Trial` license tests are located in `x-pack/test/api_integration/apis/ml`. - -1. API integration tests with `Basic` license: - - node scripts/functional_tests_server.js --config test/api_integration_basic/config.ts - node scripts/functional_test_runner.js --config test/api_integration_basic/config.ts --include-tag mlqa - - ML API integration `Basic` license tests are located in `x-pack/test/api_integration_basic/apis/ml`. + - PATH_TO_CONFIG: `test/api_integration/config.ts` + - Add `--include-tag ml` to the test runner command + - Tests are located in `x-pack/test/api_integration/apis/ml` 1. Accessibility tests: We maintain a suite of accessibility tests (you may see them referred to elsewhere as `a11y` tests). These tests render each of our pages and ensure that the inputs and other elements contain the attributes necessary to ensure all users are able to make use of ML (for example, users relying on screen readers). - node scripts/functional_tests_server --config test/accessibility/config.ts - node scripts/functional_test_runner.js --config test/accessibility/config.ts --grep=ml - - ML accessibility tests are located in `x-pack/test/accessibility/apps`. + - PATH_TO_CONFIG: `test/accessibility/config.ts` + - Add `--grep=ml` to the test runner command + - Tests are located in `x-pack/test/accessibility/apps` ## Generating docs screenshots @@ -151,7 +158,7 @@ for test server and test runner. The test server command starts an Elasticsearch and Kibana instance that the tests will be run against. node scripts/functional_tests_server.js --config test/screenshot_creation/config.ts - node scripts/functional_test_runner.js --config test/screenshot_creation/config.ts --include-tag mlqa + node scripts/functional_test_runner.js --config test/screenshot_creation/config.ts --include-tag ml The generated screenshots are stored in `x-pack/test/functional/screenshots/session/ml_docs`. ML screenshot generation tests are located in `x-pack/test/screenshot_creation/apps/ml_docs`. diff --git a/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/get_elasticsearch_settings_cluster.ts b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/get_elasticsearch_settings_cluster.ts new file mode 100644 index 0000000000000..e68a2920155a5 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/get_elasticsearch_settings_cluster.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const getElasticsearchSettingsClusterResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/get_elasticsearch_settings_nodes.ts b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/get_elasticsearch_settings_nodes.ts new file mode 100644 index 0000000000000..2621683b85d97 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/get_elasticsearch_settings_nodes.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const getElasticsearchSettingsNodesResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/index.ts b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/index.ts new file mode 100644 index 0000000000000..3268982b69b9a --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './get_elasticsearch_settings_cluster'; +export * from './get_elasticsearch_settings_nodes'; +export * from './post_elasticsearch_settings_internal_monitoring'; +export * from './put_elasticsearch_settings_collection_enabled'; +export * from './put_elasticsearch_settings_collection_interval'; diff --git a/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/post_elasticsearch_settings_internal_monitoring.ts b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/post_elasticsearch_settings_internal_monitoring.ts new file mode 100644 index 0000000000000..54b65d4c1c527 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/post_elasticsearch_settings_internal_monitoring.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { ccsRT } from '../shared'; + +export const postElasticsearchSettingsInternalMonitoringRequestPayloadRT = rt.partial({ + ccs: ccsRT, +}); + +export type PostElasticsearchSettingsInternalMonitoringRequestPayload = rt.TypeOf< + typeof postElasticsearchSettingsInternalMonitoringRequestPayloadRT +>; + +export const postElasticsearchSettingsInternalMonitoringResponsePayloadRT = rt.type({ + body: rt.type({ + legacy_indices: rt.number, + mb_indices: rt.number, + }), +}); diff --git a/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/put_elasticsearch_settings_collection_enabled.ts b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/put_elasticsearch_settings_collection_enabled.ts new file mode 100644 index 0000000000000..f65fdaddc4548 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/put_elasticsearch_settings_collection_enabled.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const putElasticsearchSettingsCollectionEnabledResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/put_elasticsearch_settings_collection_interval.ts b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/put_elasticsearch_settings_collection_interval.ts new file mode 100644 index 0000000000000..da4905c044fe0 --- /dev/null +++ b/x-pack/plugins/monitoring/common/http_api/elasticsearch_settings/put_elasticsearch_settings_collection_interval.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const putElasticsearchSettingsCollectionIntervalResponsePayloadRT = rt.type({ + // TODO: add payload entries +}); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.ts similarity index 65% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.ts index 6996c4885d25d..df2fafa2a952c 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/cluster.ts @@ -5,25 +5,25 @@ * 2.0. */ +import { getElasticsearchSettingsClusterResponsePayloadRT } from '../../../../../../common/http_api/elasticsearch_settings'; import { checkClusterSettings } from '../../../../../lib/elasticsearch_settings'; import { handleSettingsError } from '../../../../../lib/errors'; +import { MonitoringCore } from '../../../../../types'; /* * Cluster Settings Check Route */ -export function clusterSettingsCheckRoute(server) { +export function clusterSettingsCheckRoute(server: MonitoringCore) { server.route({ - method: 'GET', + method: 'get', path: '/api/monitoring/v1/elasticsearch_settings/check/cluster', - config: { - validate: {}, - }, + validate: {}, async handler(req) { try { const response = await checkClusterSettings(req); // needs to be try/catch to handle privilege error - return response; + return getElasticsearchSettingsClusterResponsePayloadRT.encode(response); } catch (err) { - console.log(err); + server.log.error(err); throw handleSettingsError(err); } }, diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts index 8bee3f273e107..11e0eec3f08f0 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/internal_monitoring.ts @@ -5,17 +5,21 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; -import { RequestHandlerContext } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { RequestHandlerContext } from '@kbn/core/server'; +import { prefixIndexPatternWithCcs } from '../../../../../../common/ccs_utils'; import { INDEX_PATTERN_ELASTICSEARCH, INDEX_PATTERN_KIBANA, INDEX_PATTERN_LOGSTASH, } from '../../../../../../common/constants'; -import { prefixIndexPatternWithCcs } from '../../../../../../common/ccs_utils'; +import { + postElasticsearchSettingsInternalMonitoringRequestPayloadRT, + postElasticsearchSettingsInternalMonitoringResponsePayloadRT, +} from '../../../../../../common/http_api/elasticsearch_settings'; +import { createValidationFunction } from '../../../../../lib/create_route_validation_function'; import { handleError } from '../../../../../lib/errors'; -import { RouteDependencies, LegacyServer } from '../../../../../types'; +import { LegacyServer, RouteDependencies } from '../../../../../types'; const queryBody = { size: 0, @@ -69,13 +73,15 @@ const checkLatestMonitoringIsLegacy = async (context: RequestHandlerContext, ind }; export function internalMonitoringCheckRoute(server: LegacyServer, npRoute: RouteDependencies) { + const validateBody = createValidationFunction( + postElasticsearchSettingsInternalMonitoringRequestPayloadRT + ); + npRoute.router.post( { path: '/api/monitoring/v1/elasticsearch_settings/check/internal_monitoring', validate: { - body: schema.object({ - ccs: schema.maybe(schema.string()), - }), + body: validateBody, }, }, async (context, request, response) => { @@ -101,9 +107,11 @@ export function internalMonitoringCheckRoute(server: LegacyServer, npRoute: Rout typeCount.mb_indices += counts.mbIndicesCount; }); - return response.ok({ - body: typeCount, - }); + return response.ok( + postElasticsearchSettingsInternalMonitoringResponsePayloadRT.encode({ + body: typeCount, + }) + ); } catch (err) { throw handleError(err); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/nodes.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/nodes.ts similarity index 67% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/nodes.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/nodes.ts index fe675302a982f..90c37c6f910c9 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/nodes.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/check/nodes.ts @@ -5,23 +5,23 @@ * 2.0. */ +import { getElasticsearchSettingsNodesResponsePayloadRT } from '../../../../../../common/http_api/elasticsearch_settings'; import { checkNodesSettings } from '../../../../../lib/elasticsearch_settings'; import { handleSettingsError } from '../../../../../lib/errors'; +import { MonitoringCore } from '../../../../../types'; /* * Cluster Settings Check Route */ -export function nodesSettingsCheckRoute(server) { +export function nodesSettingsCheckRoute(server: MonitoringCore) { server.route({ - method: 'GET', + method: 'get', path: '/api/monitoring/v1/elasticsearch_settings/check/nodes', - config: { - validate: {}, - }, + validate: {}, async handler(req) { try { const response = await checkNodesSettings(req); // needs to be try/catch to handle privilege error - return response; + return getElasticsearchSettingsNodesResponsePayloadRT.encode(response); } catch (err) { throw handleSettingsError(err); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts similarity index 100% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts index 8eb50a57fb858..61bb1ba804a5a 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -export { internalMonitoringCheckRoute } from './check/internal_monitoring'; export { clusterSettingsCheckRoute } from './check/cluster'; +export { internalMonitoringCheckRoute } from './check/internal_monitoring'; export { nodesSettingsCheckRoute } from './check/nodes'; export { setCollectionEnabledRoute } from './set/collection_enabled'; export { setCollectionIntervalRoute } from './set/collection_interval'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_enabled.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_enabled.ts similarity index 64% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_enabled.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_enabled.ts index c8bf24156f129..941818699ede2 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_enabled.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_enabled.ts @@ -5,23 +5,23 @@ * 2.0. */ +import { putElasticsearchSettingsCollectionEnabledResponsePayloadRT } from '../../../../../../common/http_api/elasticsearch_settings'; import { setCollectionEnabled } from '../../../../../lib/elasticsearch_settings'; import { handleSettingsError } from '../../../../../lib/errors'; +import { MonitoringCore } from '../../../../../types'; /* * Cluster Settings Check Route */ -export function setCollectionEnabledRoute(server) { +export function setCollectionEnabledRoute(server: MonitoringCore) { server.route({ - method: 'PUT', + method: 'put', path: '/api/monitoring/v1/elasticsearch_settings/set/collection_enabled', - config: { - validate: {}, - }, + validate: {}, async handler(req) { try { const response = await setCollectionEnabled(req); - return response; + return putElasticsearchSettingsCollectionEnabledResponsePayloadRT.encode(response); } catch (err) { throw handleSettingsError(err); } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_interval.js b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_interval.ts similarity index 64% rename from x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_interval.js rename to x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_interval.ts index 60216650062c0..eb4798efc36cc 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_interval.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/elasticsearch_settings/set/collection_interval.ts @@ -5,23 +5,23 @@ * 2.0. */ +import { putElasticsearchSettingsCollectionIntervalResponsePayloadRT } from '../../../../../../common/http_api/elasticsearch_settings'; import { setCollectionInterval } from '../../../../../lib/elasticsearch_settings'; import { handleSettingsError } from '../../../../../lib/errors'; +import { MonitoringCore } from '../../../../../types'; /* * Cluster Settings Check Route */ -export function setCollectionIntervalRoute(server) { +export function setCollectionIntervalRoute(server: MonitoringCore) { server.route({ - method: 'PUT', + method: 'put', path: '/api/monitoring/v1/elasticsearch_settings/set/collection_interval', - config: { - validate: {}, - }, + validate: {}, async handler(req) { try { const response = await setCollectionInterval(req); - return response; + return putElasticsearchSettingsCollectionIntervalResponsePayloadRT.encode(response); } catch (err) { throw handleSettingsError(err); } diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/LICENSE_OFL.txt b/x-pack/plugins/screenshotting/server/assets/fonts/noto/LICENSE_OFL.txt similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/LICENSE_OFL.txt rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/LICENSE_OFL.txt diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Medium.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Medium.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/NotoSansCJKtc-Regular.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/NotoSansCJKtc-Regular.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/index.js b/x-pack/plugins/screenshotting/server/assets/fonts/noto/index.js similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/noto/index.js rename to x-pack/plugins/screenshotting/server/assets/fonts/noto/index.js diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/LICENSE.txt b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/LICENSE.txt similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/LICENSE.txt rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/LICENSE.txt diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Italic.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Italic.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Medium.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Medium.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf b/x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/fonts/roboto/Roboto-Regular.ttf rename to x-pack/plugins/screenshotting/server/assets/fonts/roboto/Roboto-Regular.ttf diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png b/x-pack/plugins/screenshotting/server/assets/img/logo-grey.png similarity index 100% rename from x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/assets/img/logo-grey.png rename to x-pack/plugins/screenshotting/server/assets/img/logo-grey.png diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index fd09396d6c86d..66f905bd07cb2 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -17,6 +17,7 @@ import { import { ConfigType } from '../../config'; import { allowRequest } from '../network_policy'; import { stripUnsafeHeaders } from './strip_unsafe_headers'; +import { getFooterTemplate, getHeaderTemplate } from './templates'; export type Context = Record; @@ -155,6 +156,18 @@ export class HeadlessChromiumDriver { return !this.page.isClosed(); } + async printA4Pdf({ title, logo }: { title: string; logo?: string }): Promise { + return this.page.pdf({ + format: 'a4', + preferCSSPageSize: true, + scale: 1, + landscape: false, + displayHeaderFooter: true, + headerTemplate: await getHeaderTemplate({ title }), + footerTemplate: await getFooterTemplate({ logo }), + }); + } + /* * Call Page.screenshot and return a base64-encoded string of the image */ @@ -359,7 +372,7 @@ export class HeadlessChromiumDriver { // `port` is null in URLs that don't explicitly state it, // however we can derive the port from the protocol (http/https) - // IE: https://feeds-staging.elastic.co/kibana/v8.0.0.json + // IE: https://feeds.elastic.co/kibana/v8.0.0.json const derivedPort = (protocol: string | null, port: string | null, url: string) => { if (port) { return port; diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts index 7d31cdc0c6b8c..bfdc74aa43ba6 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -114,10 +114,6 @@ export class HeadlessChromiumDriverFactory { const dataDir = getDataPath(); fs.mkdirSync(dataDir, { recursive: true }); this.userDataDir = fs.mkdtempSync(path.join(dataDir, 'chromium-')); - - if (this.config.browser.chromium.disableSandbox) { - logger.warn(`Enabling the Chromium sandbox provides an additional layer of protection.`); - } } private getChromiumArgs() { diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts index d74313fa5ace1..1a04574155c1e 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts @@ -67,8 +67,8 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'x64', - archiveFilename: 'chromium-70f5d88-linux_x64.zip', - archiveChecksum: '7b1c9c2fb613444fbdf004a3b75a58df', + archiveFilename: 'chromium-70f5d88-locales-linux_x64.zip', + archiveChecksum: '759bda5e5d32533cb136a85e37c0d102', binaryChecksum: '82e80f9727a88ba3836ce230134bd126', binaryRelativePath: 'headless_shell-linux_x64/headless_shell', location: 'custom', @@ -78,8 +78,8 @@ export class ChromiumArchivePaths { { platform: 'linux', architecture: 'arm64', - archiveFilename: 'chromium-70f5d88-linux_arm64.zip', - archiveChecksum: '4a0217cfe7da86ad1e3d0e9e5895ddb5', + archiveFilename: 'chromium-70f5d88-locales-linux_arm64.zip', + archiveChecksum: '33613b8dc5212c0457210d5a37ea4b43', binaryChecksum: '29e943fbee6d87a217abd6cb6747058e', binaryRelativePath: 'headless_shell-linux_arm64/headless_shell', location: 'custom', diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/templates/footer.handlebars.html b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/footer.handlebars.html new file mode 100644 index 0000000000000..ddd85a50fc6a5 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/footer.handlebars.html @@ -0,0 +1,47 @@ + +
+ + + {{#if hasCustomLogo}} +
{{poweredByElasticCopy}}
+ {{/if}} +
+  of  +
+
diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/templates/header.handlebars.html b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/header.handlebars.html new file mode 100644 index 0000000000000..616e5f753b233 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/header.handlebars.html @@ -0,0 +1,10 @@ + +{{title}} diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/templates/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/index.ts new file mode 100644 index 0000000000000..7034dac76cfca --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/templates/index.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import fs from 'fs/promises'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { assetPath } from '../../../constants'; + +async function compileTemplate(pathToTemplate: string): Promise> { + const contentsBuffer = await fs.readFile(pathToTemplate); + return Handlebars.compile(contentsBuffer.toString()); +} + +interface HeaderTemplateInput { + title: string; +} +interface GetHeaderArgs { + title: string; +} + +export async function getHeaderTemplate({ title }: GetHeaderArgs): Promise { + const template = await compileTemplate( + path.resolve(__dirname, './header.handlebars.html') + ); + return template({ title }); +} + +async function getDefaultFooterLogo(): Promise { + const logoBuffer = await fs.readFile(path.resolve(assetPath, 'img', 'logo-grey.png')); + return `data:image/png;base64,${logoBuffer.toString('base64')}`; +} + +interface FooterTemplateInput { + base64FooterLogo: string; + hasCustomLogo: boolean; + poweredByElasticCopy: string; +} + +interface GetFooterArgs { + logo?: string; +} + +export async function getFooterTemplate({ logo }: GetFooterArgs): Promise { + const template = await compileTemplate( + path.resolve(__dirname, './footer.handlebars.html') + ); + const hasCustomLogo = Boolean(logo); + return template({ + base64FooterLogo: hasCustomLogo ? logo! : await getDefaultFooterLogo(), + hasCustomLogo, + poweredByElasticCopy: i18n.translate( + 'xpack.screenshotting.exportTypes.printablePdf.footer.logoDescription', + { + defaultMessage: 'Powered by Elastic', + } + ), + }); +} diff --git a/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts index 6805996fb1a5a..74a80cf10b58b 100644 --- a/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts @@ -88,11 +88,11 @@ describe('ensureDownloaded', () => { expect.arrayContaining([ 'chrome-mac.zip', 'chrome-win.zip', - 'chromium-70f5d88-linux_x64.zip', + 'chromium-70f5d88-locales-linux_x64.zip', ]) ); expect(readdirSync(path.resolve(`${paths.archivesPath}/arm64`))).toEqual( - expect.arrayContaining(['chrome-mac.zip', 'chromium-70f5d88-linux_arm64.zip']) + expect.arrayContaining(['chrome-mac.zip', 'chromium-70f5d88-locales-linux_arm64.zip']) ); }); diff --git a/x-pack/plugins/screenshotting/server/config/create_config.ts b/x-pack/plugins/screenshotting/server/config/create_config.ts index f12f2205d3a57..1b7076d05e478 100644 --- a/x-pack/plugins/screenshotting/server/config/create_config.ts +++ b/x-pack/plugins/screenshotting/server/config/create_config.ts @@ -24,13 +24,13 @@ export async function createConfig(parentLogger: Logger, config: ConfigType) { // disableSandbox was not set by user, apply default for OS const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); - const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' '); + const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' ').trim(); logger.debug(`Running on OS: '${osName}'`); if (disableSandbox === true) { logger.warn( - `Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS. Automatically setting 'xpack.screenshotting.capture.browser.chromium.disableSandbox: true'.` + `Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS. Automatically setting 'xpack.screenshotting.browser.chromium.disableSandbox: true'.` ); } else { logger.info( diff --git a/x-pack/plugins/screenshotting/server/constants.ts b/x-pack/plugins/screenshotting/server/constants.ts new file mode 100644 index 0000000000000..38fde163778ec --- /dev/null +++ b/x-pack/plugins/screenshotting/server/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 path from 'path'; + +export const assetPath = path.resolve(__dirname, 'assets'); diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts index ce28c53bb5f88..716b2bd46352f 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts @@ -5,6 +5,10 @@ * 2.0. */ +// FIXME: Once/if we have the ability to get page count directly from Chrome/puppeteer +// we should get rid of this lib. +import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js'; + import type { Values } from '@kbn/utility-types'; import { groupBy } from 'lodash'; import type { PackageInfo } from '@kbn/core/server'; @@ -99,30 +103,51 @@ export async function toPdf( { logo, title }: PdfScreenshotOptions, { metrics, results }: CaptureResult ): Promise { - const timeRange = getTimeRange(results); - try { - const { buffer, pages } = await pngsToPdf({ - title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined, - results, - layout, - logo, - packageInfo, - eventLogger, + let buffer: Buffer; + let pages: number; + const shouldConvertPngsToPdf = layout.id !== LayoutTypes.PRINT; + if (shouldConvertPngsToPdf) { + const timeRange = getTimeRange(results); + try { + ({ buffer, pages } = await pngsToPdf({ + title: title ? `${title}${timeRange ? ` - ${timeRange}` : ''}` : undefined, + results, + layout, + logo, + packageInfo, + eventLogger, + })); + + return { + metrics: { + ...(metrics ?? {}), + pages, + }, + data: buffer, + errors: results.flatMap(({ error }) => (error ? [error] : [])), + renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), + }; + } catch (error) { + eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`); + eventLogger.error(error, Transactions.PDF); + throw error; + } + } else { + buffer = results[0].screenshots[0].data; // This buffer is already the PDF + pages = await PDFJS.getDocument({ data: buffer }).promise.then((doc) => { + const numPages = doc.numPages; + doc.destroy(); + return numPages; }); - - return { - metrics: { - ...(metrics ?? {}), - pages, - }, - data: buffer, - errors: results.flatMap(({ error }) => (error ? [error] : [])), - renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), - }; - } catch (error) { - eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`); - eventLogger.error(error, Transactions.PDF); - - throw error; } + + return { + metrics: { + ...(metrics ?? {}), + pages, + }, + data: buffer, + errors: results.flatMap(({ error }) => (error ? [error] : [])), + renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []), + }; } diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts index 3e44a53a7f3c0..03192aacd887f 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/constants.ts @@ -5,9 +5,8 @@ * 2.0. */ -import path from 'path'; +import { assetPath } from '../../../constants'; -export const assetPath = path.resolve(__dirname, 'assets'); export const tableBorderWidth = 1; export const pageMarginTop = 40; export const pageMarginBottom = 80; @@ -21,3 +20,4 @@ export const subheadingMarginTop = 0; export const subheadingMarginBottom = 5; export const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; +export { assetPath }; diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts index afd9e294e9ae0..7dd964594ca53 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/pdfmaker.ts @@ -83,6 +83,8 @@ export class PdfMaker { const groupCount = this.content.length; // inject a page break for every 2 groups on the page + // TODO: Remove this code since we are now using Chromium to drive this + // layout via native print functionality. if (groupCount > 0 && groupCount % this.layout.groupCount === 0) { contents = [ { diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts index 033fb24c80685..64027ffbd3cf2 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts @@ -12,7 +12,7 @@ import { CaptureResult } from '..'; import { PLUGIN_ID } from '../../../common'; import { ConfigType } from '../../config'; import { ElementPosition } from '../get_element_position_data'; -import { Screenshot } from '../get_screenshots'; +import type { Screenshot } from '../types'; export enum Actions { OPEN_URL = 'open-url', @@ -25,6 +25,7 @@ export enum Actions { WAIT_RENDER = 'wait-for-render', WAIT_VISUALIZATIONS = 'wait-for-visualizations', GET_SCREENSHOT = 'get-screenshots', + PRINT_A4_PDF = 'print-a4-pdf', ADD_IMAGE = 'add-pdf-image', COMPILE = 'compile-pdf', } diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts b/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts new file mode 100644 index 0000000000000..026d62ada876c --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/get_pdf.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Actions, EventLogger } from './event_logger'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { Screenshot } from './types'; + +export async function getPdf( + browser: HeadlessChromiumDriver, + logger: EventLogger, + title: string, + logo?: string +): Promise { + logger.kbnLogger.info('printing PDF'); + + const spanEnd = logger.logPdfEvent('printing A4 PDF', Actions.PRINT_A4_PDF, 'output'); + + const result = [ + { + data: await browser.printA4Pdf({ title, logo }), + title: null, + description: null, + }, + ]; + + spanEnd(); + + return result; +} diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts index f157649bbb848..67cfbd111e750 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts @@ -8,23 +8,7 @@ import type { HeadlessChromiumDriver } from '../browsers'; import { Actions, EventLogger } from './event_logger'; import type { ElementsPositionAndAttribute } from './get_element_position_data'; - -export interface Screenshot { - /** - * Screenshot PNG image data. - */ - data: Buffer; - - /** - * Screenshot title. - */ - title: string | null; - - /** - * Screenshot description. - */ - description: string | null; -} +import type { Screenshot } from './types'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts index 5048d3f0a3be6..d06014c82ecc7 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -8,7 +8,7 @@ import type { Headers } from '@kbn/core/server'; import { defer, forkJoin, Observable, throwError } from 'rxjs'; import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; -import { errors } from '../../common'; +import { errors, LayoutTypes } from '../../common'; import type { Context, HeadlessChromiumDriver } from '../browsers'; import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers'; import { ConfigType, durationToNumber as toNumber } from '../config'; @@ -18,13 +18,15 @@ import type { ElementsPositionAndAttribute } from './get_element_position_data'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getRenderErrors } from './get_render_errors'; -import type { Screenshot } from './get_screenshots'; +import type { Screenshot } from './types'; import { getScreenshots } from './get_screenshots'; +import { getPdf } from './get_pdf'; import { getTimeRange } from './get_time_range'; import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; +import type { PdfScreenshotOptions } from '../formats'; type CaptureTimeouts = ConfigType['capture']['timeouts']; export interface PhaseTimeouts extends CaptureTimeouts { @@ -237,6 +239,26 @@ export class ScreenshotObservableHandler { ); } + /** + * Given a title and time range value look like: + * + * "[Logs] Web Traffic - Apr 14, 2022 @ 120742.318 to Apr 21, 2022 @ 120742.318" + * + * Otherwise closest thing to that or a blank string. + */ + private getTitle(timeRange: null | string): string { + return `${(this.options as PdfScreenshotOptions).title ?? ''} ${ + timeRange ? `- ${timeRange}` : '' + }`.trim(); + } + + private shouldCapturePdf(): boolean { + return ( + this.layout.id === LayoutTypes.PRINT && + (this.options as PdfScreenshotOptions).format === 'pdf' + ); + } + public getScreenshots() { return (withRenderComplete: Observable) => withRenderComplete.pipe( @@ -247,7 +269,14 @@ export class ScreenshotObservableHandler { getDefaultElementPosition(this.layout.getViewport(1)); let screenshots: Screenshot[] = []; try { - screenshots = await getScreenshots(this.driver, this.eventLogger, elements); + screenshots = this.shouldCapturePdf() + ? await getPdf( + this.driver, + this.eventLogger, + this.getTitle(data.timeRange), + (this.options as PdfScreenshotOptions).logo + ) + : await getScreenshots(this.driver, this.eventLogger, elements); } catch (e) { throw new errors.FailedToCaptureScreenshot(e.message); } diff --git a/x-pack/plugins/screenshotting/server/screenshots/types.ts b/x-pack/plugins/screenshotting/server/screenshots/types.ts new file mode 100644 index 0000000000000..d4a408313fc43 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Screenshot { + /** + * Screenshot PNG image data. + */ + data: Buffer; + + /** + * Screenshot title. + */ + title: string | null; + + /** + * Screenshot description. + */ + description: string | null; +} diff --git a/x-pack/plugins/security_solution/public/common/components/landing_cards/translations.tsx b/x-pack/plugins/security_solution/public/common/components/landing_cards/translations.tsx index 51da2e72c3bbd..fb91c358486d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_cards/translations.tsx +++ b/x-pack/plugins/security_solution/public/common/components/landing_cards/translations.tsx @@ -42,7 +42,7 @@ export const ENDPOINT_TITLE = i18n.translate( export const ENDPOINT_DESCRIPTION = i18n.translate( 'xpack.securitySolution.overview.landingCards.box.endpoint.desc', { - defaultMessage: 'Prevent, collect, detect and respond -- all with Elastic Agent.', + defaultMessage: 'Prevent, collect, detect and respond — all with Elastic Agent.', } ); @@ -55,7 +55,7 @@ export const SIEM_CARD_TITLE = i18n.translate( export const SIEM_CARD_DESCRIPTION = i18n.translate( 'xpack.securitySolution.overview.landingCards.box.siemCard.desc', { - defaultMessage: 'Detect, investigate, and respond to evolving threats', + defaultMessage: 'Detect, investigate, and respond to evolving threats.', } ); @@ -69,6 +69,6 @@ export const UNIFY_DESCRIPTION = i18n.translate( 'xpack.securitySolution.overview.landingCards.box.unify.desc', { defaultMessage: - 'Elastic Security modernizes security operations — enabling analytics across years of data, automating key processes, and bringing native endpoint security to every host.', + 'Elastic Security modernizes security operations — enabling analytics across years of data, automating key processes, and protecting every host.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index 0f613aff8d456..8cb29901abdad 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -13,6 +13,10 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; +import { initialUserPrivilegesState as mockInitialUserPrivilegesState } from '../../../../common/components/user_privileges/user_privileges_context'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; + +jest.mock('../../../../common/components/user_privileges'); const ecsRowData: Ecs = { _id: '1', @@ -71,6 +75,7 @@ const addToNewCaseButton = '[data-test-subj="add-to-new-case-action"]'; const markAsOpenButton = '[data-test-subj="open-alert-status"]'; const markAsAcknowledgedButton = '[data-test-subj="acknowledged-alert-status"]'; const markAsClosedButton = '[data-test-subj="close-alert-status"]'; +const addEndpointEventFilterButton = '[data-test-subj="add-event-filter-menu-item"]'; describe('InvestigateInResolverAction', () => { test('it render AddToCase context menu item if timelineId === TimelineId.detectionsPage', () => { @@ -107,12 +112,7 @@ describe('InvestigateInResolverAction', () => { }); test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => { - // In order to enable alert context menu without a timelineId, event needs to be event.kind === 'event' and agent.type === 'endpoint' - const customProps = { - ...props, - ecsRowData: { ...ecsRowData, agent: { type: ['endpoint'] }, event: { kind: ['event'] } }, - }; - const wrapper = mount(, { + const wrapper = mount(, { wrappingComponent: TestProviders, }); wrapper.find(actionMenuButton).simulate('click'); @@ -131,4 +131,84 @@ describe('InvestigateInResolverAction', () => { expect(wrapper.find(markAsAcknowledgedButton).first().exists()).toEqual(true); expect(wrapper.find(markAsClosedButton).first().exists()).toEqual(true); }); + + describe('AddEndpointEventFilter', () => { + const endpointEventProps = { + ...props, + ecsRowData: { ...ecsRowData, agent: { type: ['endpoint'] }, event: { kind: ['event'] } }, + }; + + describe('when users can access endpoint management', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...mockInitialUserPrivilegesState(), + endpointPrivileges: { loading: false, canAccessEndpointManagement: true }, + }); + }); + + test('it disables AddEndpointEventFilter when timeline id is not host events page', () => { + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + }); + + test('it enables AddEndpointEventFilter when timeline id is host events page', () => { + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(false); + }); + + test('it disables AddEndpointEventFilter when timeline id is host events page but is not from endpoint', () => { + const customProps = { + ...props, + ecsRowData: { ...ecsRowData, agent: { type: ['other'] }, event: { kind: ['event'] } }, + }; + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + }); + }); + describe('when users can NOT access endpoint management', () => { + beforeEach(() => { + (useUserPrivileges as jest.Mock).mockReturnValue({ + ...mockInitialUserPrivilegesState(), + endpointPrivileges: { loading: false, canAccessEndpointManagement: false }, + }); + }); + + test('it disables AddEndpointEventFilter when timeline id is host events page but cannot acces endpoint management', () => { + const wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + wrapper.find(actionMenuButton).simulate('click'); + expect(wrapper.find(addEndpointEventFilterButton).first().exists()).toEqual(true); + expect(wrapper.find(addEndpointEventFilterButton).first().props().disabled).toEqual(true); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index a6af9febe8b3e..1427b2b3bf388 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -88,6 +88,9 @@ const AlertContextMenuComponent: React.FC indexOf(ecsRowData.event?.kind, 'event') !== -1, [ecsRowData]); + const isAgentEndpoint = useMemo(() => ecsRowData.agent?.type?.includes('endpoint'), [ecsRowData]); + + const isEndpointEvent = useMemo(() => isEvent && isAgentEndpoint, [isEvent, isAgentEndpoint]); const onButtonClick = useCallback(() => { setPopover(!isPopoverOpen); @@ -173,7 +176,14 @@ const AlertContextMenuComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx index 1a56c575057f0..4327c5a69a949 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_event_filter_action.tsx @@ -12,9 +12,11 @@ import { ACTION_ADD_EVENT_FILTER } from '../translations'; export const useEventFilterAction = ({ onAddEventFilterClick, disabled = false, + tooltipMessage, }: { onAddEventFilterClick: () => void; disabled?: boolean; + tooltipMessage?: string; }) => { const eventFilterActionItems = useMemo( () => [ @@ -23,11 +25,12 @@ export const useEventFilterAction = ({ data-test-subj="add-event-filter-menu-item" onClick={onAddEventFilterClick} disabled={disabled} + toolTipContent={tooltipMessage} > {ACTION_ADD_EVENT_FILTER} , ], - [onAddEventFilterClick, disabled] + [onAddEventFilterClick, disabled, tooltipMessage] ); return { eventFilterActionItems }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index bdddd8ab46207..eba1fa8238d05 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -185,6 +185,14 @@ export const ACTION_ADD_EVENT_FILTER = i18n.translate( } ); +export const ACTION_ADD_EVENT_FILTER_DISABLED_TOOLTIP = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.actions.addEventFilter.disabled.tooltip', + { + defaultMessage: + 'Endpoint event filters can be created from the Events section of the Hosts page.', + } +); + export const ACTION_ADD_ENDPOINT_EXCEPTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException', { diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx index 4d3a03852e49f..f55e72180c5cf 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx @@ -23,7 +23,7 @@ import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { KibanaPageTemplate, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { KibanaSolutionAvatar } from '@kbn/shared-ux-components'; +import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; import type { Space } from '../../common'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../common/constants'; diff --git a/x-pack/plugins/synthetics/common/translations.ts b/x-pack/plugins/synthetics/common/translations.ts index 9bef65bd9dad6..52f0dbf5d906e 100644 --- a/x-pack/plugins/synthetics/common/translations.ts +++ b/x-pack/plugins/synthetics/common/translations.ts @@ -28,11 +28,23 @@ export const MonitorStatusTranslations = { defaultMessage: 'Monitor {monitorName} with url {monitorUrl} from {observerLocation} {statusMessage} The latest error message is {latestErrorMessage}', values: { - monitorName: '{{state.monitorName}}', - monitorUrl: '{{{state.monitorUrl}}}', - statusMessage: '{{{state.statusMessage}}}', - latestErrorMessage: '{{{state.latestErrorMessage}}}', - observerLocation: '{{state.observerLocation}}', + monitorName: '{{context.monitorName}}', + monitorUrl: '{{{context.monitorUrl}}}', + statusMessage: '{{{context.statusMessage}}}', + latestErrorMessage: '{{{context.latestErrorMessage}}}', + observerLocation: '{{context.observerLocation}}', + }, + } + ), + defaultRecoveryMessage: i18n.translate( + 'xpack.synthetics.alerts.monitorStatus.defaultRecoveryMessage', + { + defaultMessage: + 'Alert for monitor {monitorName} with url {monitorUrl} from {observerLocation} has recovered', + values: { + monitorName: '{{context.monitorName}}', + monitorUrl: '{{{context.monitorUrl}}}', + observerLocation: '{{context.observerLocation}}', }, } ), @@ -46,13 +58,19 @@ export const MonitorStatusTranslations = { export const TlsTranslations = { defaultActionMessage: i18n.translate('xpack.synthetics.alerts.tls.defaultActionMessage', { - defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} -`, + defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}`, values: { - commonName: '{{state.commonName}}', - issuer: '{{state.issuer}}', - summary: '{{state.summary}}', - status: '{{state.status}}', + commonName: '{{context.commonName}}', + issuer: '{{context.issuer}}', + summary: '{{context.summary}}', + status: '{{context.status}}', + }, + }), + defaultRecoveryMessage: i18n.translate('xpack.synthetics.alerts.tls.defaultRecoveryMessage', { + defaultMessage: `Alert for TLS certificate {commonName} from issuer {issuer} has recovered`, + values: { + commonName: '{{context.commonName}}', + issuer: '{{context.issuer}}', }, }), name: i18n.translate('xpack.synthetics.alerts.tls.clientName', { @@ -103,14 +121,27 @@ export const DurationAnomalyTranslations = { defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}. Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`, values: { - severity: '{{state.severity}}', - anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}', - monitor: '{{state.monitor}}', - monitorUrl: '{{{state.monitorUrl}}}', - slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}', - expectedResponseTime: '{{state.expectedResponseTime}}', - severityScore: '{{state.severityScore}}', - observerLocation: '{{state.observerLocation}}', + severity: '{{context.severity}}', + anomalyStartTimestamp: '{{context.anomalyStartTimestamp}}', + monitor: '{{context.monitor}}', + monitorUrl: '{{{context.monitorUrl}}}', + slowestAnomalyResponse: '{{context.slowestAnomalyResponse}}', + expectedResponseTime: '{{context.expectedResponseTime}}', + severityScore: '{{context.severityScore}}', + observerLocation: '{{context.observerLocation}}', + }, + } + ), + defaultRecoveryMessage: i18n.translate( + 'xpack.synthetics.alerts.durationAnomaly.defaultRecoveryMessage', + { + defaultMessage: `Alert for abnormal ({severity} level) response time detected on monitor {monitor} with url {monitorUrl} from location {observerLocation} at {anomalyStartTimestamp} has recovered`, + values: { + severity: '{{context.severity}}', + anomalyStartTimestamp: '{{context.anomalyStartTimestamp}}', + monitor: '{{context.monitor}}', + monitorUrl: '{{{context.monitorUrl}}}', + observerLocation: '{{context.observerLocation}}', }, } ), diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/duration_anomaly.tsx index c866ca4c76956..5f0c8c07172bb 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/duration_anomaly.tsx @@ -16,7 +16,7 @@ import { getMonitorRouteFromMonitorId } from '../../../../common/utils/get_monit import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts'; import { DurationAnomalyTranslations } from '../../../../common/translations'; -const { defaultActionMessage, description } = DurationAnomalyTranslations; +const { defaultActionMessage, defaultRecoveryMessage, description } = DurationAnomalyTranslations; const DurationAnomalyAlert = React.lazy(() => import('./lazy_wrapper/duration_anomaly')); export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ @@ -34,6 +34,7 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ description, validate: () => ({ errors: {} }), defaultActionMessage, + defaultRecoveryMessage, requiresAppContext: true, format: ({ fields }) => ({ reason: fields[ALERT_REASON] || '', diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.test.ts b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.test.ts index 2f67219ac1ae5..c4d02806b5913 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.test.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.test.ts @@ -202,7 +202,8 @@ describe('monitor status alert type', () => { }) ).toMatchInlineSnapshot(` Object { - "defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} from {{state.observerLocation}} {{{state.statusMessage}}} The latest error message is {{{state.latestErrorMessage}}}", + "defaultActionMessage": "Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}", + "defaultRecoveryMessage": "Alert for monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} has recovered", "description": "Alert when a monitor is down or an availability threshold is breached.", "documentationUrl": [Function], "format": [Function], diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.tsx index 0361e6408e43b..f7584cb04320e 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/monitor_status.tsx @@ -23,7 +23,7 @@ import { getMonitorRouteFromMonitorId } from '../../../../common/utils/get_monit import { MonitorStatusTranslations } from '../../../../common/translations'; import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts'; -const { defaultActionMessage, description } = MonitorStatusTranslations; +const { defaultActionMessage, defaultRecoveryMessage, description } = MonitorStatusTranslations; const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status')); @@ -54,6 +54,7 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({ return validateFunc ? validateFunc(ruleParams) : ({} as ValidationResult); }, defaultActionMessage, + defaultRecoveryMessage, requiresAppContext: false, format: ({ fields }) => ({ reason: fields[ALERT_REASON] || '', diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/tls.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/tls.tsx index 2c1238028ccf5..b9ab025ecc021 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/tls.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/alert_types/tls.tsx @@ -14,7 +14,7 @@ import { AlertTypeInitializer } from '.'; import { CERTIFICATES_ROUTE } from '../../../../common/constants/ui'; -const { defaultActionMessage, description } = TlsTranslations; +const { defaultActionMessage, defaultRecoveryMessage, description } = TlsTranslations; const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); export const initTlsAlertType: AlertTypeInitializer = ({ core, @@ -29,6 +29,7 @@ export const initTlsAlertType: AlertTypeInitializer = ({ description, validate: () => ({ errors: {} }), defaultActionMessage, + defaultRecoveryMessage, requiresAppContext: false, format: ({ fields }) => ({ reason: fields[ALERT_REASON] || '', diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.test.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.test.ts index 16c49d7c3afcb..068cdfd90b1ae 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.test.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.test.ts @@ -50,7 +50,7 @@ describe('Alert Actions factory', () => { eventAction: 'trigger', severity: 'error', summary: - 'Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} from {{state.observerLocation}} {{{state.statusMessage}}} The latest error message is {{{state.latestErrorMessage}}}', + 'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}', }, id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9', }, @@ -75,7 +75,7 @@ describe('Alert Actions factory', () => { eventAction: 'trigger', severity: 'error', summary: - 'Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} from {{state.observerLocation}} {{{state.statusMessage}}} The latest error message is {{{state.latestErrorMessage}}}', + 'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}', }, }, ]); @@ -93,7 +93,7 @@ describe('Alert Actions factory', () => { eventAction: 'trigger', severity: 'error', summary: - 'Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} from {{state.observerLocation}} {{{state.statusMessage}}} The latest error message is {{{state.latestErrorMessage}}}', + 'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}', }, id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9', }, @@ -118,7 +118,7 @@ describe('Alert Actions factory', () => { eventAction: 'trigger', severity: 'error', summary: - 'Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} from {{state.observerLocation}} {{{state.statusMessage}}} The latest error message is {{{state.latestErrorMessage}}}', + 'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}', }, }, ]); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.ts index eabfe42691e8d..31d8c0577780c 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/alert_actions.ts @@ -127,11 +127,11 @@ function getIndexActionParams(selectedMonitor: Ping, recovery = false): IndexAct return { documents: [ { - monitorName: '{{state.monitorName}}', - monitorUrl: '{{{state.monitorUrl}}}', + monitorName: '{{context.monitorName}}', + monitorUrl: '{{{context.monitorUrl}}}', statusMessage: getRecoveryMessage(selectedMonitor), latestErrorMessage: '', - observerLocation: '{{state.observerLocation}}', + observerLocation: '{{context.observerLocation}}', }, ], indexOverride: null, @@ -140,11 +140,11 @@ function getIndexActionParams(selectedMonitor: Ping, recovery = false): IndexAct return { documents: [ { - monitorName: '{{state.monitorName}}', - monitorUrl: '{{{state.monitorUrl}}}', - statusMessage: '{{{state.statusMessage}}}', - latestErrorMessage: '{{{state.latestErrorMessage}}}', - observerLocation: '{{state.observerLocation}}', + monitorName: '{{context.monitorName}}', + monitorUrl: '{{{context.monitorUrl}}}', + statusMessage: '{{{context.statusMessage}}}', + latestErrorMessage: '{{{context.latestErrorMessage}}}', + observerLocation: '{{context.observerLocation}}', }, ], indexOverride: null, diff --git a/x-pack/plugins/synthetics/server/lib/alerts/common.ts b/x-pack/plugins/synthetics/server/lib/alerts/common.ts index 8381adce21d2c..f370b258b482f 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/common.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/common.ts @@ -8,6 +8,7 @@ import { isRight } from 'fp-ts/lib/Either'; import Mustache from 'mustache'; import { IBasePath } from '@kbn/core/server'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types'; export type UpdateUptimeAlertState = ( @@ -59,9 +60,17 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => { }; export const generateAlertMessage = (messageTemplate: string, fields: Record) => { - return Mustache.render(messageTemplate, { state: { ...fields } }); + return Mustache.render(messageTemplate, { context: { ...fields }, state: { ...fields } }); }; export const getViewInAppUrl = (relativeViewInAppUrl: string, basePath: IBasePath) => basePath.publicBaseUrl ? new URL(basePath.prepend(relativeViewInAppUrl), basePath.publicBaseUrl).toString() : relativeViewInAppUrl; + +export const setRecoveredAlertsContext = (alertFactory: RuleExecutorServices['alertFactory']) => { + const { getRecoveredAlerts } = alertFactory.done(); + for (const alert of getRecoveredAlerts()) { + const state = alert.getState(); + alert.setContext(state); + } +}; diff --git a/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.test.ts index eb4509850414b..ad821a509b77b 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.test.ts @@ -12,7 +12,6 @@ import { import { durationAnomalyAlertFactory } from './duration_anomaly'; import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { AnomaliesTableRecord, AnomalyRecordDoc } from '@kbn/ml-plugin/common/types/anomalies'; -import { DynamicSettings } from '../../../common/runtime_types'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { getSeverityType } from '@kbn/ml-plugin/common/util/anomaly_utils'; import { Ping } from '../../../common/runtime_types/ping'; @@ -33,34 +32,6 @@ interface MockAnomalyResult { const monitorId = 'uptime-monitor'; const mockUrl = 'https://elastic.co'; -/** - * This function aims to provide an easy way to give mock props that will - * reduce boilerplate for tests. - * @param dynamic the expiration and aging thresholds received at alert creation time - * @param params the params received at alert creation time - * @param state the state the alert maintains - */ -const mockOptions = ( - dynamicCertSettings?: { - certExpirationThreshold: DynamicSettings['certExpirationThreshold']; - certAgeThreshold: DynamicSettings['certAgeThreshold']; - }, - state = {}, - params = { - timerange: { from: 'now-15m', to: 'now' }, - monitorId, - severity: 'warning', - } -): any => { - const { services } = createRuleTypeMocks(dynamicCertSettings); - - return { - params, - state, - services, - }; -}; - const mockAnomaliesResult: MockAnomalyResult = { anomalies: [ { @@ -94,6 +65,50 @@ const mockPing: Partial = { }, }; +const mockRecoveredAlerts = mockAnomaliesResult.anomalies.map((result) => ({ + firstCheckedAt: 'date', + firstTriggeredAt: undefined, + lastCheckedAt: 'date', + lastResolvedAt: undefined, + isTriggered: false, + anomalyStartTimestamp: 'date', + currentTriggerStarted: undefined, + expectedResponseTime: `${Math.round(result.typicalSort / 1000)} ms`, + lastTriggeredAt: undefined, + monitor: monitorId, + monitorUrl: mockPing.url?.full, + observerLocation: result.entityValue, + severity: getSeverityType(result.severity), + severityScore: result.severity, + slowestAnomalyResponse: `${Math.round(result.actualSort / 1000)} ms`, + bucketSpan: result.source.bucket_span, +})); + +/** + * This function aims to provide an easy way to give mock props that will + * reduce boilerplate for tests. + * @param dynamic the expiration and aging thresholds received at alert creation time + * @param params the params received at alert creation time + * @param state the state the alert maintains + */ +const mockOptions = ( + state = {}, + params = { + timerange: { from: 'now-15m', to: 'now' }, + monitorId, + severity: 'warning', + } +): any => { + const { services, setContext } = createRuleTypeMocks(mockRecoveredAlerts); + + return { + params, + state, + services, + setContext, + }; +}; + describe('duration anomaly alert', () => { let toISOStringSpy: jest.SpyInstance; const mockDate = 'date'; @@ -206,7 +221,7 @@ Response times as high as ${slowestResponse} ms have been detected from location )} level) response time detected on uptime-monitor with url ${ mockPing.url?.full } at date. Anomaly severity score is ${anomaly.severity}. - Response times as high as ${slowestResponse} ms have been detected from location ${ +Response times as high as ${slowestResponse} ms have been detected from location ${ anomaly.entityValue }. Expected response time is ${typicalResponse} ms.`; @@ -218,7 +233,17 @@ Response times as high as ${slowestResponse} ms have been detected from location Array [ "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { - "${ALERT_REASON_MSG}": "${reasonMessages[0]}", + "anomalyStartTimestamp": "date", + "bucketSpan": 900, + "expectedResponseTime": "10 ms", + "monitor": "uptime-monitor", + "monitorUrl": "https://elastic.co", + "observerLocation": "harrisburg", + "${ALERT_REASON_MSG}": "Abnormal (minor level) response time detected on uptime-monitor with url https://elastic.co at date. Anomaly severity score is 25. + Response times as high as 200 ms have been detected from location harrisburg. Expected response time is 10 ms.", + "severity": "minor", + "severityScore": 25, + "slowestAnomalyResponse": "200 ms", "${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MA==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", }, ] @@ -227,11 +252,52 @@ Response times as high as ${slowestResponse} ms have been detected from location Array [ "xpack.uptime.alerts.actionGroups.durationAnomaly", Object { - "${ALERT_REASON_MSG}": "${reasonMessages[1]}", + "anomalyStartTimestamp": "date", + "bucketSpan": 900, + "expectedResponseTime": "20 ms", + "monitor": "uptime-monitor", + "monitorUrl": "https://elastic.co", + "observerLocation": "fairbanks", + "${ALERT_REASON_MSG}": "Abnormal (warning level) response time detected on uptime-monitor with url https://elastic.co at date. Anomaly severity score is 10. + Response times as high as 300 ms have been detected from location fairbanks. Expected response time is 20 ms.", + "severity": "warning", + "severityScore": 10, + "slowestAnomalyResponse": "300 ms", "${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z", }, ] `); }); + + it('sets alert recovery context for recovered alerts', async () => { + toISOStringSpy.mockImplementation(() => mockDate); + const mockResultServiceProviderGetter: jest.Mock<{ + getAnomaliesTableData: jest.Mock; + }> = jest.fn(); + const mockGetAnomliesTableDataGetter: jest.Mock = jest.fn(); + const mockGetLatestMonitorGetter: jest.Mock> = jest.fn(); + + mockGetLatestMonitorGetter.mockReturnValue(mockPing); + mockGetAnomliesTableDataGetter.mockReturnValue(mockAnomaliesResult); + mockResultServiceProviderGetter.mockReturnValue({ + getAnomaliesTableData: mockGetAnomliesTableDataGetter, + }); + const { server, libs, plugins } = bootstrapDependencies( + { getLatestMonitor: mockGetLatestMonitorGetter }, + { + ml: { + resultsServiceProvider: mockResultServiceProviderGetter, + }, + } + ); + const alert = durationAnomalyAlertFactory(server, libs, plugins); + const options = mockOptions(); + // @ts-ignore the executor can return `void`, but ours never does + const state: Record = await alert.executor(options); + expect(options.setContext).toHaveBeenCalledTimes(2); + mockRecoveredAlerts.forEach((alertState) => { + expect(options.setContext).toHaveBeenCalledWith(alertState); + }); + }); }); }); diff --git a/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.ts index f2ec05b11f5ea..a93d44013708b 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/duration_anomaly.ts @@ -15,7 +15,12 @@ import { import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; import { AnomaliesTableRecord } from '@kbn/ml-plugin/common/types/anomalies'; import { getSeverityType } from '@kbn/ml-plugin/common/util/anomaly_utils'; -import { updateState, generateAlertMessage, getViewInAppUrl } from './common'; +import { + updateState, + generateAlertMessage, + getViewInAppUrl, + setRecoveredAlertsContext, +} from './common'; import { CLIENT_ALERT_TYPES, DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { UptimeCorePluginsSetup } from '../adapters/framework'; @@ -94,14 +99,26 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory }, ], actionVariables: { - context: [ACTION_VARIABLES[ALERT_REASON_MSG], ACTION_VARIABLES[VIEW_IN_APP_URL]], + context: [ + ACTION_VARIABLES[ALERT_REASON_MSG], + ACTION_VARIABLES[VIEW_IN_APP_URL], + ...durationAnomalyTranslations.actionVariables, + ...commonStateTranslations, + ], state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations], }, isExportable: true, minimumLicenseRequired: 'platinum', + doesSetRecoveryContext: true, async executor({ params, - services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient, getAlertStartedDate }, + services: { + alertWithLifecycle, + scopedClusterClient, + savedObjectsClient, + getAlertStartedDate, + alertFactory, + }, state, startedAt, }) { @@ -160,10 +177,13 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory alertInstance.scheduleActions(DURATION_ANOMALY.id, { [ALERT_REASON_MSG]: alertReasonMessage, [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), + ...summary, }); }); } + setRecoveredAlertsContext(alertFactory); + return updateState(state, foundAnomalies); }, }); diff --git a/x-pack/plugins/synthetics/server/lib/alerts/status_check.test.ts b/x-pack/plugins/synthetics/server/lib/alerts/status_check.test.ts index 84e7c0d68400c..b9a90ee18038a 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/status_check.test.ts @@ -56,6 +56,53 @@ const mockMonitors = [ }, ]; +const mockRecoveredAlerts = [ + { + currentTriggerStarted: '2022-04-25T14:36:31.511Z', + firstCheckedAt: '2022-04-25T14:10:30.785Z', + firstTriggeredAt: '2022-04-25T14:10:30.785Z', + lastCheckedAt: '2022-04-25T14:36:31.511Z', + lastTriggeredAt: '2022-04-25T14:36:31.511Z', + lastResolvedAt: '2022-04-25T14:23:43.007Z', + isTriggered: true, + monitorUrl: 'https://expired.badssl.com/', + monitorId: 'expired-badssl', + monitorName: 'BadSSL Expired', + monitorType: 'http', + latestErrorMessage: + 'Get "https://expired.badssl.com/": x509: certificate has expired or is not yet valid: current time 2022-04-25T10:36:27-04:00 is after 2015-04-12T23:59:59Z', + observerLocation: 'Unnamed-location', + observerHostname: 'Dominiques-MacBook-Pro-2.local', + reason: + 'BadSSL Expired from Unnamed-location failed 2 times in the last 3 mins. Alert when > 1.', + statusMessage: 'failed 2 times in the last 3 mins. Alert when > 1.', + start: '2022-04-25T14:36:31.621Z', + duration: 315110000000, + }, + { + currentTriggerStarted: '2022-04-25T14:36:31.511Z', + firstCheckedAt: '2022-04-25T14:10:30.785Z', + firstTriggeredAt: '2022-04-25T14:10:30.785Z', + lastCheckedAt: '2022-04-25T14:36:31.511Z', + lastTriggeredAt: '2022-04-25T14:36:31.511Z', + lastResolvedAt: '2022-04-25T14:23:43.007Z', + isTriggered: true, + monitorUrl: 'https://invalid.badssl.com/', + monitorId: 'expired-badssl', + monitorName: 'BadSSL Expired', + monitorType: 'http', + latestErrorMessage: + 'Get "https://invalid.badssl.com/": x509: certificate has expired or is not yet valid: current time 2022-04-25T10:36:27-04:00 is after 2015-04-12T23:59:59Z', + observerLocation: 'Unnamed-location', + observerHostname: 'Dominiques-MacBook-Pro-2.local', + reason: + 'BadSSL Expired from Unnamed-location failed 2 times in the last 3 mins. Alert when > 1.', + statusMessage: 'failed 2 times in the last 3 mins. Alert when > 1.', + start: '2022-04-25T14:36:31.621Z', + duration: 315110000000, + }, +]; + const mockCommonAlertDocumentFields = (monitorInfo: GetMonitorStatusResult['monitorInfo']) => ({ 'agent.name': monitorInfo.agent?.name, 'error.message': monitorInfo.error?.message, @@ -121,13 +168,14 @@ const mockOptions = ( }, } ): any => { - const { services } = createRuleTypeMocks(); + const { services, setContext } = createRuleTypeMocks(mockRecoveredAlerts); return { params, state, services, rule, + setContext, }; }; @@ -142,6 +190,7 @@ describe('status check alert', () => { afterEach(() => { jest.clearAllMocks(); }); + describe('executor', () => { it('does not trigger when there are no monitors down', async () => { expect.assertions(5); @@ -242,7 +291,15 @@ describe('status check alert', () => { Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { + "latestErrorMessage": "error message 1", + "monitorId": "first", + "monitorName": "First", + "monitorType": "myType", + "monitorUrl": "localhost:8080", + "observerHostname": undefined, + "observerLocation": "harrisburg", "reason": "First from harrisburg failed 234 times in the last 15 mins. Alert when > 5.", + "statusMessage": "failed 234 times in the last 15 mins. Alert when > 5.", "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ] @@ -313,7 +370,15 @@ describe('status check alert', () => { Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { + "latestErrorMessage": "error message 1", + "monitorId": "first", + "monitorName": "First", + "monitorType": "myType", + "monitorUrl": "localhost:8080", + "observerHostname": undefined, + "observerLocation": "harrisburg", "reason": "First from harrisburg failed 234 times in the last 15m. Alert when > 5.", + "statusMessage": "failed 234 times in the last 15m. Alert when > 5.", "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ] @@ -785,28 +850,60 @@ describe('status check alert', () => { Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { + "latestErrorMessage": undefined, + "monitorId": "foo", + "monitorName": "Foo", + "monitorType": "myType", + "monitorUrl": "https://foo.com", + "observerHostname": undefined, + "observerLocation": "harrisburg", "reason": "Foo from harrisburg 35 days availability is 99.28%. Alert when < 99.34%.", + "statusMessage": "35 days availability is 99.28%. Alert when < 99.34%.", "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { + "latestErrorMessage": undefined, + "monitorId": "foo", + "monitorName": "Foo", + "monitorType": "myType", + "monitorUrl": "https://foo.com", + "observerHostname": undefined, + "observerLocation": "fairbanks", "reason": "Foo from fairbanks 35 days availability is 98.03%. Alert when < 99.34%.", + "statusMessage": "35 days availability is 98.03%. Alert when < 99.34%.", "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { + "latestErrorMessage": undefined, + "monitorId": "unreliable", + "monitorName": "Unreliable", + "monitorType": "myType", + "monitorUrl": "https://unreliable.co", + "observerHostname": undefined, + "observerLocation": "fairbanks", "reason": "Unreliable from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + "statusMessage": "35 days availability is 90.92%. Alert when < 99.34%.", "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/dW5yZWxpYWJsZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], Array [ "xpack.uptime.alerts.actionGroups.monitorStatus", Object { + "latestErrorMessage": undefined, + "monitorId": "no-name", + "monitorName": "no-name", + "monitorType": "myType", + "monitorUrl": "https://no-name.co", + "observerHostname": undefined, + "observerLocation": "fairbanks", "reason": "no-name from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.", + "statusMessage": "35 days availability is 90.92%. Alert when < 99.34%.", "viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/bm8tbmFtZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D", }, ], @@ -909,6 +1006,26 @@ describe('status check alert', () => { }) ); }); + + it('sets alert recovery context for recovered alerts', async () => { + toISOStringSpy.mockImplementation(() => 'foo date string'); + const mockGetter: jest.Mock = jest.fn(); + + mockGetter.mockReturnValue(mockMonitors); + const { server, libs, plugins } = bootstrapDependencies({ getMonitorStatus: mockGetter }); + const alert = statusCheckAlertFactory(server, libs, plugins); + const options = mockOptions(); + // @ts-ignore the executor can return `void`, but ours never does + const state: Record = await alert.executor(options); + expect(options.setContext).toHaveBeenCalledTimes(2); + mockRecoveredAlerts.forEach((alertState) => { + expect(options.setContext).toHaveBeenCalledWith(alertState); + }); + }); + }); + + describe('alert recovery', () => { + it('sets context for alert recovery', () => {}); }); describe('alert factory', () => { diff --git a/x-pack/plugins/synthetics/server/lib/alerts/status_check.ts b/x-pack/plugins/synthetics/server/lib/alerts/status_check.ts index d305dedea3e10..243749f686106 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/status_check.ts @@ -21,7 +21,7 @@ import { GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; import { CLIENT_ALERT_TYPES, MONITOR_STATUS } from '../../../common/constants/alerts'; -import { updateState, getViewInAppUrl } from './common'; +import { updateState, getViewInAppUrl, setRecoveredAlertsContext } from './common'; import { commonMonitorStateI18, commonStateTranslations, @@ -47,6 +47,7 @@ import { import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url'; export type ActionGroupIds = ActionGroupIdsOf; + /** * Returns the appropriate range for filtering the documents by `@timestamp`. * @@ -75,22 +76,6 @@ export function getTimestampRange({ }; } -const getMonIdByLoc = (monitorId: string, location: string) => { - return monitorId + '-' + location; -}; - -const uniqueDownMonitorIds = (items: GetMonitorStatusResult[]): Set => - items.reduce( - (acc, { monitorId, location }) => acc.add(getMonIdByLoc(monitorId, location)), - new Set() - ); - -const uniqueAvailMonitorIds = (items: GetMonitorAvailabilityResult[]): Set => - items.reduce( - (acc, { monitorId, location }) => acc.add(getMonIdByLoc(monitorId, location)), - new Set() - ); - export const getUniqueIdsByLoc = ( downMonitorsByLocation: GetMonitorStatusResult[], availabilityResults: GetMonitorAvailabilityResult[] @@ -161,7 +146,7 @@ export const getMonitorSummary = (monitorInfo: Ping, statusMessage: string) => { return { ...summary, - reason: `${monitorName} from ${observerLocation} ${statusMessage}`, + [ALERT_REASON_MSG]: `${monitorName} from ${observerLocation} ${statusMessage}`, }; }; @@ -222,6 +207,22 @@ export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => { return `${urlText}_${monIdByLoc}`; }; +const getMonIdByLoc = (monitorId: string, location: string) => { + return monitorId + '-' + location; +}; + +const uniqueDownMonitorIds = (items: GetMonitorStatusResult[]): Set => + items.reduce( + (acc, { monitorId, location }) => acc.add(getMonIdByLoc(monitorId, location)), + new Set() + ); + +const uniqueAvailMonitorIds = (items: GetMonitorAvailabilityResult[]): Set => + items.reduce( + (acc, { monitorId, location }) => acc.add(getMonIdByLoc(monitorId, location)), + new Set() + ); + export const statusCheckAlertFactory: UptimeAlertTypeFactory = (server, libs) => ({ id: CLIENT_ALERT_TYPES.MONITOR_STATUS, producer: 'uptime', @@ -281,15 +282,23 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( ACTION_VARIABLES[MONITOR_WITH_GEO], ACTION_VARIABLES[ALERT_REASON_MSG], ACTION_VARIABLES[VIEW_IN_APP_URL], + ...commonMonitorStateI18, ], state: [...commonMonitorStateI18, ...commonStateTranslations], }, isExportable: true, minimumLicenseRequired: 'basic', + doesSetRecoveryContext: true, async executor({ params: rawParams, state, - services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle, getAlertStartedDate }, + services: { + savedObjectsClient, + scopedClusterClient, + alertWithLifecycle, + getAlertStartedDate, + alertFactory, + }, rule: { schedule: { interval }, }, @@ -314,14 +323,12 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( }); const filterString = await formatFilterString(uptimeEsClient, filters, search, libs); - const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`; // Range filter for `monitor.timespan`, the range of time the ping is valid const timespanRange = oldVersionTimeRange || { from: `now-${timespanInterval}`, to: 'now', }; - // Range filter for `@timestamp`, the time the document was indexed const timestampRange = getTimestampRange({ ruleScheduleLookback: `now-${interval}`, @@ -364,10 +371,14 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( fields: getMonitorAlertDocument(monitorSummary), }); - alert.replaceState({ - ...state, + const context = { ...monitorSummary, statusMessage, + }; + + alert.replaceState({ + ...state, + ...context, ...updateState(state, true), }); @@ -381,10 +392,11 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( }); alert.scheduleActions(MONITOR_STATUS.id, { - [ALERT_REASON_MSG]: monitorSummary.reason, [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), + ...context, }); } + setRecoveredAlertsContext(alertFactory); return updateState(state, downMonitorsByLocation.length > 0); } @@ -436,11 +448,16 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( fields: getMonitorAlertDocument(monitorSummary), }); - alert.replaceState({ - ...updateState(state, true), + const context = { ...monitorSummary, statusMessage, + }; + + alert.replaceState({ + ...updateState(state, true), + ...context, }); + const relativeViewInAppUrl = getMonitorRouteFromMonitorId({ monitorId: monitorSummary.monitorId, dateRangeEnd: 'now', @@ -451,10 +468,11 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( }); alert.scheduleActions(MONITOR_STATUS.id, { - [ALERT_REASON_MSG]: monitorSummary.reason, [VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath), + ...context, }); }); + setRecoveredAlertsContext(alertFactory); return updateState(state, downMonitorsByLocation.length > 0); }, }); diff --git a/x-pack/plugins/synthetics/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/synthetics/server/lib/alerts/test_utils/index.ts index af248af730eee..456b0675eee87 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/test_utils/index.ts @@ -13,8 +13,6 @@ import { UMServerLibs } from '../../lib'; import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../adapters'; import type { UptimeRouter } from '../../../types'; import { getUptimeESMockClient } from '../../requests/helper'; -import { DynamicSettings } from '../../../../common/runtime_types'; -import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; /** * The alert takes some dependencies as parameters; these are things like @@ -41,15 +39,7 @@ export const bootstrapDependencies = (customRequests?: any, customPlugins: any = return { server, libs, plugins }; }; -export const createRuleTypeMocks = ( - dynamicCertSettings: { - certAgeThreshold: DynamicSettings['certAgeThreshold']; - certExpirationThreshold: DynamicSettings['certExpirationThreshold']; - } = { - certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, - certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, - } -) => { +export const createRuleTypeMocks = (recoveredAlerts: Array> = []) => { const loggerMock = { debug: jest.fn(), warn: jest.fn(), @@ -58,10 +48,17 @@ export const createRuleTypeMocks = ( const scheduleActions = jest.fn(); const replaceState = jest.fn(); + const setContext = jest.fn(); const services = { ...getUptimeESMockClient(), ...alertsMock.createRuleExecutorServices(), + alertFactory: { + ...alertsMock.createRuleExecutorServices().alertFactory, + done: () => ({ + getRecoveredAlerts: () => createRecoveredAlerts(recoveredAlerts, setContext), + }), + }, alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }), getAlertStartedDate: jest.fn().mockReturnValue('2022-03-17T13:13:33.755Z'), logger: loggerMock, @@ -77,5 +74,14 @@ export const createRuleTypeMocks = ( services, scheduleActions, replaceState, + setContext, }; }; + +const createRecoveredAlerts = (alerts: Array>, setContext: jest.Mock) => { + return alerts.map((alert) => ({ + getState: () => alert, + setContext, + context: {}, + })); +}; diff --git a/x-pack/plugins/synthetics/server/lib/alerts/tls.test.ts b/x-pack/plugins/synthetics/server/lib/alerts/tls.test.ts index 31a5e98bf9f02..88f8b964eb590 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/tls.test.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/tls.test.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { tlsAlertFactory, getCertSummary } from './tls'; import { TLS } from '../../../common/constants/alerts'; -import { CertResult, DynamicSettings } from '../../../common/runtime_types'; +import { CertResult } from '../../../common/runtime_types'; import { createRuleTypeMocks, bootstrapDependencies } from './test_utils'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; @@ -19,24 +19,6 @@ import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects/sav * @param params the params received at alert creation time * @param state the state the alert maintains */ -const mockOptions = ( - dynamicCertSettings?: { - certExpirationThreshold: DynamicSettings['certExpirationThreshold']; - certAgeThreshold: DynamicSettings['certAgeThreshold']; - }, - state = {} -): any => { - const { services } = createRuleTypeMocks(dynamicCertSettings); - const params = { - timerange: { from: 'now-15m', to: 'now' }, - }; - - return { - params, - state, - services, - }; -}; const mockCertResult: CertResult = { certs: [ @@ -76,6 +58,35 @@ const mockCertResult: CertResult = { total: 4, }; +const mockRecoveredAlerts = [ + { + commonName: mockCertResult.certs[0].common_name ?? '', + issuer: mockCertResult.certs[0].issuer ?? '', + summary: 'sample summary', + status: 'expired', + }, + { + commonName: mockCertResult.certs[1].common_name ?? '', + issuer: mockCertResult.certs[1].issuer ?? '', + summary: 'sample summary 2', + status: 'aging', + }, +]; + +const mockOptions = (state = {}): any => { + const { services, setContext } = createRuleTypeMocks(mockRecoveredAlerts); + const params = { + timerange: { from: 'now-15m', to: 'now' }, + }; + + return { + params, + state, + services, + setContext, + }; +}; + describe('tls alert', () => { let toISOStringSpy: jest.SpyInstance; let savedObjectsAdapterSpy: jest.SpyInstance< @@ -131,16 +142,18 @@ describe('tls alert', () => { const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results; expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4); mockCertResult.certs.forEach((cert) => { - expect(alertInstanceMock.replaceState).toBeCalledWith( - expect.objectContaining({ - commonName: cert.common_name, - issuer: cert.issuer, - status: 'expired', - }) + const context = { + commonName: cert.common_name, + issuer: cert.issuer, + status: 'expired', + }; + expect(alertInstanceMock.replaceState).toBeCalledWith(expect.objectContaining(context)); + expect(alertInstanceMock.scheduleActions).toBeCalledWith( + TLS.id, + expect.objectContaining(context) ); }); expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4); - expect(alertInstanceMock.scheduleActions).toBeCalledWith(TLS.id); }); it('handles dynamic settings for aging or expiration threshold', async () => { @@ -167,6 +180,22 @@ describe('tls alert', () => { }) ); }); + + it('sets alert recovery context for recovered alerts', async () => { + toISOStringSpy.mockImplementation(() => 'foo date string'); + const mockGetter: jest.Mock = jest.fn(); + + mockGetter.mockReturnValue(mockCertResult); + const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter }); + const alert = tlsAlertFactory(server, libs, plugins); + const options = mockOptions(); + // @ts-ignore the executor can return `void`, but ours never does + const state: Record = await alert.executor(options); + expect(options.setContext).toHaveBeenCalledTimes(2); + mockRecoveredAlerts.forEach((alertState) => { + expect(options.setContext).toHaveBeenCalledWith(alertState); + }); + }); }); describe('getCertSummary', () => { diff --git a/x-pack/plugins/synthetics/server/lib/alerts/tls.ts b/x-pack/plugins/synthetics/server/lib/alerts/tls.ts index 0a6fb24c88156..127171eab0f4d 100644 --- a/x-pack/plugins/synthetics/server/lib/alerts/tls.ts +++ b/x-pack/plugins/synthetics/server/lib/alerts/tls.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { ALERT_REASON } from '@kbn/rule-data-utils'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; import { UptimeAlertTypeFactory } from './types'; -import { updateState, generateAlertMessage } from './common'; +import { updateState, generateAlertMessage, setRecoveredAlertsContext } from './common'; import { CLIENT_ALERT_TYPES, TLS } from '../../../common/constants/alerts'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; @@ -108,13 +108,14 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, }, ], actionVariables: { - context: [], + context: [...tlsTranslations.actionVariables, ...commonStateTranslations], state: [...tlsTranslations.actionVariables, ...commonStateTranslations], }, isExportable: true, minimumLicenseRequired: 'basic', + doesSetRecoveryContext: true, async executor({ - services: { alertWithLifecycle, savedObjectsClient, scopedClusterClient }, + services: { alertWithLifecycle, savedObjectsClient, scopedClusterClient, alertFactory }, state, }) { const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); @@ -173,10 +174,12 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, ...updateState(state, foundCerts), ...summary, }); - alertInstance.scheduleActions(TLS.id); + alertInstance.scheduleActions(TLS.id, { ...summary }); }); } + setRecoveredAlertsContext(alertFactory); + return updateState(state, foundCerts); }, }); diff --git a/x-pack/plugins/transform/readme.md b/x-pack/plugins/transform/readme.md index 51a89f224fb29..8a0ea7eb4f660 100644 --- a/x-pack/plugins/transform/readme.md +++ b/x-pack/plugins/transform/readme.md @@ -106,8 +106,8 @@ and Kibana instance that the tests will be run against. 1. Functional UI tests with `Trial` license (default config): - node scripts/functional_tests_server.js - node scripts/functional_test_runner.js --include-tag transform + node scripts/functional_tests_server.js --config test/functional/apps/transform/config.ts + node scripts/functional_test_runner.js --config test/functional/apps/transform/config.ts Transform functional `Trial` license tests are located in `x-pack/test/functional/apps/transform`. @@ -120,7 +120,7 @@ and Kibana instance that the tests will be run against. 1. API integration tests with `Trial` license: - node scripts/functional_tests_server.js + node scripts/functional_tests_server.js --config test/api_integration/config.ts node scripts/functional_test_runner.js --config test/api_integration/config.ts --include-tag transform Transform API integration `Trial` license tests are located in `x-pack/test/api_integration/apis/transform`. diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f23d9cf64202b..8a30c8bb80de3 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9716,7 +9716,6 @@ "xpack.cases.recentCases.viewAllCasesLink": "Afficher tous les cas", "xpack.cases.settings.syncAlertsSwitchLabelOff": "Arrêt", "xpack.cases.settings.syncAlertsSwitchLabelOn": "Marche", - "xpack.cases.status.all": "Tout", "xpack.cases.status.closed": "Fermé", "xpack.cases.status.iconAria": "Modifier le statut", "xpack.cases.status.inProgress": "En cours", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b42d92c275e9..fb1ca2dcd650a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9812,7 +9812,6 @@ "xpack.cases.recentCases.viewAllCasesLink": "すべてのケースを表示", "xpack.cases.settings.syncAlertsSwitchLabelOff": "オフ", "xpack.cases.settings.syncAlertsSwitchLabelOn": "オン", - "xpack.cases.status.all": "すべて", "xpack.cases.status.closed": "終了", "xpack.cases.status.iconAria": "ステータスの変更", "xpack.cases.status.inProgress": "進行中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dd87bbaa23fef..3b96df93ad35d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9834,7 +9834,6 @@ "xpack.cases.recentCases.viewAllCasesLink": "查看所有案例", "xpack.cases.settings.syncAlertsSwitchLabelOff": "关闭", "xpack.cases.settings.syncAlertsSwitchLabelOn": "开启", - "xpack.cases.status.all": "全部", "xpack.cases.status.closed": "已关闭", "xpack.cases.status.iconAria": "更改状态", "xpack.cases.status.inProgress": "进行中", diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index 4d64f02d2c14b..4928b368a96b4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -95,6 +95,12 @@ export interface DrilldownDefinition< */ isConfigValid: ActionFactoryDefinition['isConfigValid']; + /** + * Compatibility check during drilldown creation. + * Could be used to filter out a drilldown if it's not compatible with the current context. + */ + isConfigurable?(context: FactoryContext): boolean; + /** * Name of EUI icon to display when showing this drilldown to user. */ diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx index db9951f235dfc..f52ac6e161577 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/containers/action_factory_picker/action_factory_picker.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { ActionFactoryPicker as ActionFactoryPickerUi } from '../../../../components/action_factory_picker'; import { useDrilldownManager } from '../context'; import { ActionFactoryView } from '../action_factory_view'; @@ -14,14 +15,19 @@ export const ActionFactoryPicker: React.FC = ({}) => { const drilldowns = useDrilldownManager(); const factory = drilldowns.useActionFactory(); const context = React.useMemo(() => drilldowns.getActionFactoryContext(), [drilldowns]); + const compatibleFactories = drilldowns.useCompatibleActionFactories(context); if (!!factory) { return ; } + if (!compatibleFactories) { + return ; + } + return ( { drilldowns.setActionFactory(actionFactory); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts index 15997355a2ae2..231057a50ee1f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts @@ -6,9 +6,10 @@ */ import useObservable from 'react-use/lib/useObservable'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import type { SerializableRecord } from '@kbn/utility-types'; +import { useMemo } from 'react'; import { PublicDrilldownManagerProps, DrilldownManagerDependencies, @@ -255,6 +256,24 @@ export class DrilldownManagerState { return context; } + public getCompatibleActionFactories( + context: BaseActionFactoryContext + ): Observable { + const compatibleActionFactories$ = new BehaviorSubject(undefined); + Promise.allSettled( + this.deps.actionFactories.map((factory) => factory.isCompatible(context)) + ).then((factoryCompatibility) => { + compatibleActionFactories$.next( + this.deps.actionFactories.filter((_factory, i) => { + const result = factoryCompatibility[i]; + // treat failed isCompatible checks as non-compatible + return result.status === 'fulfilled' && result.value; + }) + ); + }); + return compatibleActionFactories$.asObservable(); + } + /** * Get state object of the drilldown which is currently being created. */ @@ -478,4 +497,9 @@ export class DrilldownManagerState { public readonly useActionFactory = () => useObservable(this.actionFactory$, this.actionFactory$.getValue()); public readonly useEvents = () => useObservable(this.events$, this.events$.getValue()); + public readonly useCompatibleActionFactories = (context: BaseActionFactoryContext) => + useObservable( + useMemo(() => this.getCompatibleActionFactories(context), [context]), + undefined + ); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 63f90d5a55a1f..fb2dc3ea5bd03 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -116,6 +116,7 @@ export class UiActionsServiceEnhancements licenseFeatureName, supportedTriggers, isCompatible, + isConfigurable, telemetry, extract, inject, @@ -135,7 +136,7 @@ export class UiActionsServiceEnhancements extract, inject, getIconType: () => euiIcon, - isCompatible: async () => true, + isCompatible: async (context) => !isConfigurable || isConfigurable(context), create: (serializedAction) => ({ id: '', type: factoryId, diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 6a36bf756cf19..c504811fcd0ce 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('Machine Learning', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/cases_api_integration/common/lib/mock.ts b/x-pack/test/cases_api_integration/common/lib/mock.ts index 08f30f8df024e..c77c4ff8d6451 100644 --- a/x-pack/test/cases_api_integration/common/lib/mock.ts +++ b/x-pack/test/cases_api_integration/common/lib/mock.ts @@ -30,6 +30,7 @@ export const postCaseReq: CasePostRequest = { description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', tags: ['defacement'], + severity: CaseSeverity.LOW, connector: { id: 'none', name: 'none', diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index ddf0425fb5386..0381c46214669 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; import { CASES_URL } from '@kbn/cases-plugin/common/constants'; -import { CaseResponse, CaseStatuses, CommentType } from '@kbn/cases-plugin/common/api'; +import { + CaseResponse, + CaseSeverity, + CaseStatuses, + CommentType, +} from '@kbn/cases-plugin/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -117,6 +122,45 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('filters by severity', async () => { + await createCase(supertest, postCaseReq); + const theCase = await createCase(supertest, postCaseReq); + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: theCase.id, + version: theCase.version, + severity: CaseSeverity.HIGH, + }, + ], + }, + }); + + const cases = await findCases({ supertest, query: { severity: CaseSeverity.HIGH } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [patchedCase[0]], + count_open_cases: 1, + }); + }); + + it('filters by severity (none found)', async () => { + await createCase(supertest, postCaseReq); + await createCase(supertest, postCaseReq); + + const cases = await findCases({ supertest, query: { severity: CaseSeverity.CRITICAL } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 0, + cases: [], + }); + }); + it('filters by reporters', async () => { const postedCase = await createCase(supertest, postCaseReq); const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); @@ -802,6 +846,55 @@ export default ({ getService }: FtrProviderContext): void => { ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); }); }); + + describe('RBAC query filter', () => { + it('should return the correct cases when trying to query filter by severity', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', severity: CaseSeverity.HIGH }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', severity: CaseSeverity.HIGH }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', severity: CaseSeverity.HIGH }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution should get only the security solution cases + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + severity: CaseSeverity.HIGH, + }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 2, ['securitySolutionFixture']); + }); + }); }); }); }; diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts index ba0d030cfcf6f..c809d0ee5c20d 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; interface Detector { identifier: string; @@ -220,7 +220,7 @@ export default function ({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('advanced job', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts index 78974ecf1e64c..0740c365f02e2 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/aggregated_scripted_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts @@ -6,7 +6,7 @@ */ import { Datafeed, Job } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -360,7 +360,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('aggregated or scripted job', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts index 17c576281835a..05e38d565e969 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts @@ -6,14 +6,14 @@ */ import { Annotation } from '@kbn/ml-plugin/common/types/annotations'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); describe('annotations', function () { - this.tags(['mlqa']); + this.tags(['ml']); const jobId = `fq_single_1_smv_${Date.now()}`; const annotation = { diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index c71f4a5789fd2..6e3e98171b109 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { @@ -64,7 +64,7 @@ export default function ({ getService }: FtrProviderContext) { const elasticChart = getService('elasticChart'); describe('anomaly explorer', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index 96c02f7827a58..2ee9d226596d8 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -6,7 +6,7 @@ */ import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '@kbn/ml-plugin/common/constants/categorization_job'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -75,7 +75,7 @@ export default function ({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('categorization', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/categorization_small'); await ml.testResources.createIndexPatternIfNeeded('ft_categorization_small', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/config.ts b/x-pack/test/functional/apps/ml/anomaly_detection/config.ts similarity index 85% rename from x-pack/test/functional/apps/ml/group2/config.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/config.ts index d927f93adeffd..9078782e36f0b 100644 --- a/x-pack/test/functional/apps/ml/group2/config.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/config.ts @@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML anomaly_detection', + }, }; } diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts similarity index 97% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts index 1e6e020aff69c..7920cf9721d47 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/custom_urls.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts @@ -7,12 +7,12 @@ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE } from '@kbn/ml-plugin/public/application/jobs/components/custom_url_editor/constants'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import type { DiscoverUrlConfig, DashboardUrlConfig, OtherUrlConfig, -} from '../../../../services/ml/job_table'; +} from '../../../services/ml/job_table'; // @ts-expect-error doesn't implement the full interface const JOB_CONFIG: Job = { @@ -63,7 +63,7 @@ export default function ({ getService }: FtrProviderContext) { const browser = getService('browser'); describe('custom urls', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index 4b593aacbebf1..ed9f63be66dd4 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; interface Detector { identifier: string; @@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('job on data set with date_nanos time field', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/event_rate_nanos'); await ml.testResources.createIndexPatternIfNeeded( diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts similarity index 97% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts index b290789419ed8..93ec331230a8a 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/forecasts.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('forecasts', function () { - this.tags(['mlqa']); + this.tags(['ml']); describe('with single metric job', function () { before(async () => { diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts similarity index 51% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/index.ts index a1127c0e71c77..0b206bfc450f3 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts @@ -5,12 +5,35 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -export default function ({ loadTestFile }: FtrProviderContext) { - describe('anomaly detection', function () { +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - anomaly detection', function () { this.tags(['skipFirefox']); + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); + + await ml.testResources.resetKibanaTimeZone(); + }); + loadTestFile(require.resolve('./single_metric_job')); loadTestFile(require.resolve('./single_metric_job_without_datafeed_start')); loadTestFile(require.resolve('./multi_metric_job')); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index 783312b0d8608..dcb47b205bb1b 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -72,7 +72,7 @@ export default function ({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('multi metric', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index af2573e21f93d..0d04bb2ff7064 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -86,7 +86,7 @@ export default function ({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('population', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts index 72dbac602cf8f..7d9c528d763d7 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -266,7 +266,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('saved search', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index e698dd270e1a8..cb21f8de77bd2 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -72,7 +72,7 @@ export default function ({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('single metric', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts index 2afa284fcc3d7..4cdea1a726fe9 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_job_without_datafeed_start.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -57,7 +57,7 @@ export default function ({ getService }: FtrProviderContext) { } describe('single metric without datafeed start', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts rename to x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts index b970a0efe5602..809ebf204e2a7 100644 --- a/x-pack/test/functional/apps/ml/group2/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts @@ -6,7 +6,7 @@ */ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; // @ts-expect-error not full interface const JOB_CONFIG: Job = { @@ -41,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('single metric viewer', function () { - this.tags(['mlqa']); + this.tags(['ml']); describe('with single metric job', function () { before(async () => { diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 0cf7c4177f057..2ba4ac6f08350 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts index cfba10c25b17b..67550ae17a4b0 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/classification_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 82f76e66b4ebd..3a33c95edba42 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { DeepPartial } from '@kbn/ml-plugin/common/types/common'; import { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/config.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/config.ts new file mode 100644 index 0000000000000..e82782f89973e --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML data_frame_analytics', + }, + }; +} diff --git a/x-pack/test/functional/apps/ml/group1/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts similarity index 64% rename from x-pack/test/functional/apps/ml/group1/index.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index 7129f3e24d4f1..19844632cc411 100644 --- a/x-pack/test/functional/apps/ml/group1/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -11,7 +11,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('machine learning - group 2', () => { + describe('machine learning - data frame analytics', function () { + this.tags(['ml', 'skipFirefox']); + before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -24,22 +26,21 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); + loadTestFile(require.resolve('./outlier_detection_creation')); + loadTestFile(require.resolve('./regression_creation')); + loadTestFile(require.resolve('./classification_creation')); + loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./results_view_content')); + loadTestFile(require.resolve('./regression_creation_saved_search')); + loadTestFile(require.resolve('./classification_creation_saved_search')); + loadTestFile(require.resolve('./outlier_detection_creation_saved_search')); }); } diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index e9146ce548422..947cd82cdd342 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts index 1e428531e6aa9..1dc431c74a97d 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/outlier_detection_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index a0cbd123b5169..7a84c41aa4a66 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts index 6b09b35c610a0..e22c4908486d1 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/regression_creation_saved_search.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation_saved_search.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AnalyticsTableRowDetails } from '../../../../services/ml/data_frame_analytics_table'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts rename to x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts index 8d04c4897dab0..2bddf0a7d9512 100644 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/results_view_content.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts @@ -8,7 +8,7 @@ import { DeepPartial } from '@kbn/ml-plugin/common/types/common'; import { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/config.ts b/x-pack/test/functional/apps/ml/data_visualizer/config.ts index d927f93adeffd..daad4e85a1f8b 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/config.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/config.ts @@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML data_visualizer', + }, }; } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index ef15775f86204..5e529a3430606 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -208,7 +208,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('file based', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts index 973ebf2bbe3ab..a75fc8d0bf794 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning - data visualizer', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); before(async () => { await ml.securityCommon.createMlRoles(); @@ -27,14 +27,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.securityCommon.cleanMlRoles(); await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); + await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_logs'); await ml.testResources.resetKibanaTimeZone(); }); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 4334e72e9a16e..1f4c20ea6faa5 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -154,7 +154,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { } describe('index based', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts index b3f0e9e175d7a..c7e00f8ed5b54 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -12,7 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('index based actions panel on trial license', function () { - this.tags(['mlqa']); + this.tags(['ml']); const indexPatternName = 'ft_farequote'; diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts index 6ddf3bba3a81f..0017a71a086fe 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts @@ -173,7 +173,7 @@ export default function ({ getService }: FtrProviderContext) { } describe('data view management', function () { - this.tags(['mlqa']); + this.tags(['ml']); const indexPatternTitle = 'ft_farequote'; before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts deleted file mode 100644 index cf9bd17f11b81..0000000000000 --- a/x-pack/test/functional/apps/ml/group1/data_frame_analytics/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('data frame analytics', function () { - this.tags(['mlqa', 'skipFirefox']); - - loadTestFile(require.resolve('./outlier_detection_creation')); - loadTestFile(require.resolve('./regression_creation')); - loadTestFile(require.resolve('./classification_creation')); - loadTestFile(require.resolve('./cloning')); - loadTestFile(require.resolve('./results_view_content')); - loadTestFile(require.resolve('./regression_creation_saved_search')); - loadTestFile(require.resolve('./classification_creation_saved_search')); - loadTestFile(require.resolve('./outlier_detection_creation_saved_search')); - }); -} diff --git a/x-pack/test/functional/apps/ml/group1/permissions/index.ts b/x-pack/test/functional/apps/ml/group1/permissions/index.ts deleted file mode 100644 index 23d7d6fe9e2b5..0000000000000 --- a/x-pack/test/functional/apps/ml/group1/permissions/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('permissions', function () { - this.tags(['skipFirefox']); - - loadTestFile(require.resolve('./full_ml_access')); - loadTestFile(require.resolve('./read_ml_access')); - loadTestFile(require.resolve('./no_ml_access')); - }); -} diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts b/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts deleted file mode 100644 index 4c4bedfeb9b76..0000000000000 --- a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('stack management jobs', function () { - this.tags(['mlqa', 'skipFirefox']); - - loadTestFile(require.resolve('./synchronize')); - loadTestFile(require.resolve('./manage_spaces')); - loadTestFile(require.resolve('./import_jobs')); - loadTestFile(require.resolve('./export_jobs')); - }); -} diff --git a/x-pack/test/functional/apps/ml/group3/config.ts b/x-pack/test/functional/apps/ml/permissions/config.ts similarity index 86% rename from x-pack/test/functional/apps/ml/group3/config.ts rename to x-pack/test/functional/apps/ml/permissions/config.ts index d927f93adeffd..cc9fffd2c93f5 100644 --- a/x-pack/test/functional/apps/ml/group3/config.ts +++ b/x-pack/test/functional/apps/ml/permissions/config.ts @@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML permission', + }, }; } diff --git a/x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts rename to x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index c632ae48b3f88..18a6e130daed0 100644 --- a/x-pack/test/functional/apps/ml/group1/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../services/ml/security_common'; +import { USER } from '../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('for user with full ML access', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); describe('with no data loaded', function () { for (const testUser of testUsers) { @@ -122,7 +122,7 @@ export default function ({ getService }: FtrProviderContext) { const ecExpectedTotalCount = '287'; const uploadFilePath = require.resolve( - '../../data_visualizer/files_to_import/artificial_server_log' + '../data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional/apps/ml/group3/index.ts b/x-pack/test/functional/apps/ml/permissions/index.ts similarity index 59% rename from x-pack/test/functional/apps/ml/group3/index.ts rename to x-pack/test/functional/apps/ml/permissions/index.ts index e85b95b274720..8b28c9e6ccda4 100644 --- a/x-pack/test/functional/apps/ml/group3/index.ts +++ b/x-pack/test/functional/apps/ml/permissions/index.ts @@ -11,7 +11,9 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('machine learning - group 3', function () { + describe('machine learning - permissions', function () { + this.tags(['ml', 'skipFirefox']); + before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -25,21 +27,14 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.securityCommon.cleanMlRoles(); await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./settings')); - loadTestFile(require.resolve('./embeddables')); - loadTestFile(require.resolve('./stack_management_jobs')); + loadTestFile(require.resolve('./full_ml_access')); + loadTestFile(require.resolve('./read_ml_access')); + loadTestFile(require.resolve('./no_ml_access')); }); } diff --git a/x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts similarity index 92% rename from x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts rename to x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 4a1c108b2fa5a..1974a48e77841 100644 --- a/x-pack/test/functional/apps/ml/group1/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../services/ml/security_common'; +import { USER } from '../../../services/ml/security_common'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'error']); @@ -16,7 +16,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testUsers = [{ user: USER.ML_UNAUTHORIZED, discoverAvailable: true }]; describe('for user with no ML access', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); for (const testUser of testUsers) { describe(`(${testUser.user})`, function () { diff --git a/x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts rename to x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index a18a6075055a6..301fc5102a94f 100644 --- a/x-pack/test/functional/apps/ml/group1/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { USER } from '../../../../services/ml/security_common'; +import { USER } from '../../../services/ml/security_common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('for user with read ML access', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); describe('with no data loaded', function () { for (const testUser of testUsers) { @@ -116,7 +116,7 @@ export default function ({ getService }: FtrProviderContext) { const ecExpectedTotalCount = '287'; const uploadFilePath = require.resolve( - '../../data_visualizer/files_to_import/artificial_server_log' + '../data_visualizer/files_to_import/artificial_server_log' ); const expectedUploadFileTitle = 'artificial_server_log'; diff --git a/x-pack/test/functional/apps/ml/group1/config.ts b/x-pack/test/functional/apps/ml/short_tests/config.ts similarity index 86% rename from x-pack/test/functional/apps/ml/group1/config.ts rename to x-pack/test/functional/apps/ml/short_tests/config.ts index d927f93adeffd..33d37ecd71457 100644 --- a/x-pack/test/functional/apps/ml/group1/config.ts +++ b/x-pack/test/functional/apps/ml/short_tests/config.ts @@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML short_tests', + }, }; } diff --git a/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts b/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_charts_dashboard_embeddables.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts rename to x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_charts_dashboard_embeddables.ts index 68981de99fc9a..ef674c1744a51 100644 --- a/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_charts_dashboard_embeddables.ts +++ b/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_charts_dashboard_embeddables.ts @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']); describe('anomaly charts in dashboard', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts b/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_embeddables_migration.ts similarity index 99% rename from x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts rename to x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_embeddables_migration.ts index a4c50549f5aed..8f3c30a15e543 100644 --- a/x-pack/test/functional/apps/ml/group3/embeddables/anomaly_embeddables_migration.ts +++ b/x-pack/test/functional/apps/ml/short_tests/embeddables/anomaly_embeddables_migration.ts @@ -66,7 +66,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']); describe('anomaly embeddables migration in Dashboard', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/group3/embeddables/constants.ts b/x-pack/test/functional/apps/ml/short_tests/embeddables/constants.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/embeddables/constants.ts rename to x-pack/test/functional/apps/ml/short_tests/embeddables/constants.ts diff --git a/x-pack/test/functional/apps/ml/group3/embeddables/index.ts b/x-pack/test/functional/apps/ml/short_tests/embeddables/index.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/embeddables/index.ts rename to x-pack/test/functional/apps/ml/short_tests/embeddables/index.ts diff --git a/x-pack/test/functional/apps/ml/group3/feature_controls/index.ts b/x-pack/test/functional/apps/ml/short_tests/feature_controls/index.ts similarity index 93% rename from x-pack/test/functional/apps/ml/group3/feature_controls/index.ts rename to x-pack/test/functional/apps/ml/short_tests/feature_controls/index.ts index ab0988c424761..657eb86e20c19 100644 --- a/x-pack/test/functional/apps/ml/group3/feature_controls/index.ts +++ b/x-pack/test/functional/apps/ml/short_tests/feature_controls/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); loadTestFile(require.resolve('./ml_security')); loadTestFile(require.resolve('./ml_spaces')); }); diff --git a/x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts b/x-pack/test/functional/apps/ml/short_tests/feature_controls/ml_security.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/feature_controls/ml_security.ts rename to x-pack/test/functional/apps/ml/short_tests/feature_controls/ml_security.ts diff --git a/x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/ml/short_tests/feature_controls/ml_spaces.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/feature_controls/ml_spaces.ts rename to x-pack/test/functional/apps/ml/short_tests/feature_controls/ml_spaces.ts diff --git a/x-pack/test/functional/apps/ml/short_tests/index.ts b/x-pack/test/functional/apps/ml/short_tests/index.ts new file mode 100644 index 0000000000000..3c4cbbc0677be --- /dev/null +++ b/x-pack/test/functional/apps/ml/short_tests/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('machine learning - short tests', function () { + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + }); + + after(async () => { + // NOTE: Logout needs to happen before anything else to avoid flaky behavior + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./model_management')); + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./embeddables')); + }); +} diff --git a/x-pack/test/functional/apps/ml/group1/model_management/index.ts b/x-pack/test/functional/apps/ml/short_tests/model_management/index.ts similarity index 92% rename from x-pack/test/functional/apps/ml/group1/model_management/index.ts rename to x-pack/test/functional/apps/ml/short_tests/model_management/index.ts index 5595486260dee..c20957beb1ea5 100644 --- a/x-pack/test/functional/apps/ml/group1/model_management/index.ts +++ b/x-pack/test/functional/apps/ml/short_tests/model_management/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('model management', function () { - this.tags(['mlqa', 'skipFirefox']); + this.tags(['ml', 'skipFirefox']); loadTestFile(require.resolve('./model_list')); }); diff --git a/x-pack/test/functional/apps/ml/group1/model_management/model_list.ts b/x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group1/model_management/model_list.ts rename to x-pack/test/functional/apps/ml/short_tests/model_management/model_list.ts diff --git a/x-pack/test/functional/apps/ml/group1/pages.ts b/x-pack/test/functional/apps/ml/short_tests/pages.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group1/pages.ts rename to x-pack/test/functional/apps/ml/short_tests/pages.ts index 2cc271e67194e..d81b5933d77df 100644 --- a/x-pack/test/functional/apps/ml/group1/pages.ts +++ b/x-pack/test/functional/apps/ml/short_tests/pages.ts @@ -11,7 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('page navigation', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); before(async () => { await ml.api.cleanMlIndices(); await ml.securityUI.loginAsMlPowerUser(); diff --git a/x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/short_tests/settings/calendar_creation.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/calendar_creation.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/calendar_creation.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts b/x-pack/test/functional/apps/ml/short_tests/settings/calendar_delete.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/calendar_delete.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/calendar_delete.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/short_tests/settings/calendar_edit.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/calendar_edit.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/calendar_edit.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/common.ts b/x-pack/test/functional/apps/ml/short_tests/settings/common.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/common.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/common.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts b/x-pack/test/functional/apps/ml/short_tests/settings/filter_list_creation.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/filter_list_creation.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/filter_list_creation.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts b/x-pack/test/functional/apps/ml/short_tests/settings/filter_list_delete.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/filter_list_delete.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/filter_list_delete.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts b/x-pack/test/functional/apps/ml/short_tests/settings/filter_list_edit.ts similarity index 100% rename from x-pack/test/functional/apps/ml/group3/settings/filter_list_edit.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/filter_list_edit.ts diff --git a/x-pack/test/functional/apps/ml/group3/settings/index.ts b/x-pack/test/functional/apps/ml/short_tests/settings/index.ts similarity index 95% rename from x-pack/test/functional/apps/ml/group3/settings/index.ts rename to x-pack/test/functional/apps/ml/short_tests/settings/index.ts index 9ac25b7fc9483..d3f7000918a8e 100644 --- a/x-pack/test/functional/apps/ml/group3/settings/index.ts +++ b/x-pack/test/functional/apps/ml/short_tests/settings/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('settings', function () { - this.tags(['mlqa', 'skipFirefox']); + this.tags(['ml', 'skipFirefox']); loadTestFile(require.resolve('./calendar_creation')); loadTestFile(require.resolve('./calendar_edit')); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/config.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/config.ts new file mode 100644 index 0000000000000..9d0fe82b9158c --- /dev/null +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML stack_management_jobs', + }, + }; +} diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts rename to x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts index 4ced89e35d608..c43cf74e3048c 100644 --- a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/export_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/export_jobs.ts @@ -7,7 +7,7 @@ import { Job, Datafeed } from '@kbn/ml-plugin/common/types/anomaly_detection_jobs'; import type { DataFrameAnalyticsConfig } from '@kbn/ml-plugin/public/application/data_frame_analytics/common'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; const testADJobs: Array<{ job: Job; datafeed: Datafeed }> = [ { @@ -255,7 +255,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('export jobs', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await ml.api.cleanMlIndices(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json rename to x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/anomaly_detection_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/bad_data.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json similarity index 100% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/bad_data.json rename to x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/bad_data.json diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json b/x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json similarity index 100% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json rename to x-pack/test/functional/apps/ml/stack_management_jobs/files_to_import/data_frame_analytics_jobs_7.16.json diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts similarity index 97% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts rename to x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index 212bb029b6e0b..e2ba704f5e109 100644 --- a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -6,7 +6,7 @@ */ import { JobType } from '@kbn/ml-plugin/common/types/saved_objects'; -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -31,7 +31,7 @@ export default function ({ getService }: FtrProviderContext) { ]; describe('import jobs', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await ml.api.cleanMlIndices(); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); diff --git a/x-pack/test/functional/apps/ml/group2/index.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts similarity index 69% rename from x-pack/test/functional/apps/ml/group2/index.ts rename to x-pack/test/functional/apps/ml/stack_management_jobs/index.ts index 4515715327e05..37f238dbeecc9 100644 --- a/x-pack/test/functional/apps/ml/group2/index.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/index.ts @@ -11,7 +11,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('machine learning - group 2', () => { + describe('machine learning - stack management jobs', function () { + this.tags(['ml', 'skipFirefox']); before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -25,18 +26,16 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.securityCommon.cleanMlRoles(); await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/categorization_small'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/event_rate_nanos'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/bm_classification'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/ihp_outlier'); await esArchiver.unload('x-pack/test/functional/es_archives/ml/egs_regression'); - await esArchiver.unload('x-pack/test/functional/es_archives/ml/module_sample_ecommerce'); await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./anomaly_detection')); + loadTestFile(require.resolve('./synchronize')); + loadTestFile(require.resolve('./manage_spaces')); + loadTestFile(require.resolve('./import_jobs')); + loadTestFile(require.resolve('./export_jobs')); }); } diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts rename to x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts index 5563bb9043c7f..e68502f4dab5a 100644 --- a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/manage_spaces.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const browser = getService('browser'); @@ -107,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) { } describe('manage spaces', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); diff --git a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts similarity index 98% rename from x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts rename to x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts index e760549b7a151..317a71ae79a0b 100644 --- a/x-pack/test/functional/apps/ml/group3/stack_management_jobs/synchronize.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/synchronize.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const dfaJobIdES = 'ihp_od_es'; describe('synchronize', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); diff --git a/x-pack/test/functional/apps/transform/config.ts b/x-pack/test/functional/apps/transform/config.ts index d0d07ff200281..17a471848867e 100644 --- a/x-pack/test/functional/apps/transform/config.ts +++ b/x-pack/test/functional/apps/transform/config.ts @@ -13,5 +13,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - Transform', + }, }; } diff --git a/x-pack/test/functional/services/cases/common.ts b/x-pack/test/functional/services/cases/common.ts index 7722c32f79837..5ba0c4dbbaa57 100644 --- a/x-pack/test/functional/services/cases/common.ts +++ b/x-pack/test/functional/services/cases/common.ts @@ -7,12 +7,14 @@ import expect from '@kbn/expect'; import { CaseStatuses } from '@kbn/cases-plugin/common'; +import { CaseSeverity } from '@kbn/cases-plugin/common/api'; import { FtrProviderContext } from '../../ftr_provider_context'; export function CasesCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const header = getPageObject('header'); + const common = getPageObject('common'); return { /** @@ -58,5 +60,13 @@ export function CasesCommonServiceProvider({ getService, getPageObject }: FtrPro await label.click(); await this.assertRadioGroupValue(testSubject, value); }, + + async selectSeverity(severity: CaseSeverity) { + await common.clickAndValidate( + 'case-severity-selection', + `case-severity-selection-${severity}` + ); + await testSubjects.click(`case-severity-selection-${severity}`); + }, }; } diff --git a/x-pack/test/functional/services/cases/create.ts b/x-pack/test/functional/services/cases/create.ts index 5ed22ad51ad9f..536badeee56a6 100644 --- a/x-pack/test/functional/services/cases/create.ts +++ b/x-pack/test/functional/services/cases/create.ts @@ -5,10 +5,12 @@ * 2.0. */ +import { CaseSeverity } from '@kbn/cases-plugin/common/api'; import uuid from 'uuid'; import { FtrProviderContext } from '../../ftr_provider_context'; export function CasesCreateViewServiceProvider({ getService, getPageObject }: FtrProviderContext) { + const common = getPageObject('common'); const testSubjects = getService('testSubjects'); const find = getService('find'); const comboBox = getService('comboBox'); @@ -39,10 +41,12 @@ export function CasesCreateViewServiceProvider({ getService, getPageObject }: Ft title = 'test-' + uuid.v4(), description = 'desc' + uuid.v4(), tag = 'tagme', + severity = CaseSeverity.LOW, }: { title: string; description: string; tag: string; + severity: CaseSeverity; }) { // case name await testSubjects.setValue('input', title); @@ -54,6 +58,11 @@ export function CasesCreateViewServiceProvider({ getService, getPageObject }: Ft const descriptionArea = await find.byCssSelector('textarea.euiMarkdownEditorTextArea'); await descriptionArea.focus(); await descriptionArea.type(description); + await common.clickAndValidate( + 'case-severity-selection', + `case-severity-selection-${severity}` + ); + await testSubjects.click(`case-severity-selection-${severity}`); // save await testSubjects.click('create-case-submit'); diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 651f52434e55f..f4d7103db0a61 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { CaseStatuses } from '@kbn/cases-plugin/common'; +import { CaseSeverityWithAll } from '@kbn/cases-plugin/common/ui'; import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -126,6 +127,11 @@ export function CasesTableServiceProvider({ getService, getPageObject }: FtrProv await testSubjects.click(`case-status-filter-${status}`); }, + async filterBySeverity(severity: CaseSeverityWithAll) { + await common.clickAndValidate('case-severity-filter', `case-severity-filter-${severity}`); + await testSubjects.click(`case-severity-filter-${severity}`); + }, + async filterByReporter(reporter: string) { await common.clickAndValidate( 'options-filter-popover-button-Reporter', diff --git a/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts b/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts index a2a135b8cef0c..2fcdf957f8909 100644 --- a/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts +++ b/x-pack/test/functional_basic/apps/ml/data_visualizer/index_data_visualizer_actions_panel.ts @@ -12,7 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('index based actions panel on basic license', function () { - this.tags(['mlqa']); + this.tags(['ml']); const indexPatternName = 'ft_farequote'; const savedSearch = 'ft_farequote_kuery'; diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index 0188aa0361d94..dbdab2cc0a4b2 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['skipFirefox', 'mlqa']); + this.tags(['skipFirefox', 'ml']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts index c5aed361aba3e..c4a7fad8224ea 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/create_case_form.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; +import { CaseSeverity } from '@kbn/cases-plugin/common/api'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getService }: FtrProviderContext) => { @@ -30,6 +31,7 @@ export default ({ getService }: FtrProviderContext) => { title: caseTitle, description: 'test description', tag: 'tagme', + severity: CaseSeverity.HIGH, }); // validate title diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts index c64f1514b7c45..b05763cfcf079 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/list_view.ts @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { CaseStatuses } from '@kbn/cases-plugin/common'; +import { CaseSeverity } from '@kbn/cases-plugin/common/api'; +import { SeverityAll } from '@kbn/cases-plugin/common/ui'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObject, getService }: FtrProviderContext) => { @@ -143,6 +145,51 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); + describe('severity filtering', () => { + before(async () => { + await cases.api.createCase({ severity: CaseSeverity.LOW }); + await cases.api.createCase({ severity: CaseSeverity.LOW }); + await cases.api.createCase({ severity: CaseSeverity.HIGH }); + await cases.api.createCase({ severity: CaseSeverity.HIGH }); + await cases.api.createCase({ severity: CaseSeverity.CRITICAL }); + await header.waitUntilLoadingHasFinished(); + await cases.casesTable.waitForCasesToBeListed(); + }); + beforeEach(async () => { + /** + * There is no easy way to clear the filtering. + * Refreshing the page seems to be easier. + */ + await cases.navigation.navigateToApp(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + await cases.casesTable.waitForCasesToBeDeleted(); + }); + + it('filters cases by severity', async () => { + // by default filter by all + await cases.casesTable.validateCasesTableHasNthRows(5); + + // low + await cases.casesTable.filterBySeverity(CaseSeverity.LOW); + await cases.casesTable.validateCasesTableHasNthRows(2); + + // high + await cases.casesTable.filterBySeverity(CaseSeverity.HIGH); + await cases.casesTable.validateCasesTableHasNthRows(2); + + // critical + await cases.casesTable.filterBySeverity(CaseSeverity.CRITICAL); + await cases.casesTable.validateCasesTableHasNthRows(1); + + // back to all + await cases.casesTable.filterBySeverity(SeverityAll); + await cases.casesTable.validateCasesTableHasNthRows(5); + }); + }); + describe('pagination', () => { before(async () => { await cases.api.createNthRandomCases(8); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts index a175e10fb7d18..9aaf523de6638 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/view_case.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; import { CaseStatuses } from '@kbn/cases-plugin/common'; +import { CaseSeverity } from '@kbn/cases-plugin/common/api'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObject, getService }: FtrProviderContext) => { @@ -184,9 +185,35 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await common.clickAndValidate('property-actions-ellipses', 'property-actions-trash'); await common.clickAndValidate('property-actions-trash', 'confirmModalConfirmButton'); await testSubjects.click('confirmModalConfirmButton'); - await testSubjects.existOrFail('cases-all-title', { timeout: 2000 }); + await header.waitUntilLoadingHasFinished(); await cases.casesTable.validateCasesTableHasNthRows(0); }); }); + + describe('Severity field', () => { + before(async () => { + await cases.navigation.navigateToApp(); + await cases.api.createNthRandomCases(1); + await cases.casesTable.waitForCasesToBeListed(); + await cases.casesTable.goToFirstListedCase(); + await header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await cases.api.deleteAllCases(); + }); + + it('shows the severity field on the sidebar', async () => { + await testSubjects.existOrFail('case-severity-selection'); + }); + it('changes the severity level from the selector', async () => { + await cases.common.selectSeverity(CaseSeverity.MEDIUM); + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('case-severity-selection-' + CaseSeverity.MEDIUM); + + // validate user action + await find.byCssSelector('[data-test-subj*="severity-update-action"]'); + }); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/ml/index.ts b/x-pack/test/functional_with_es_ssl/apps/ml/index.ts index fac9e46dcb65b..942416c73b357 100644 --- a/x-pack/test/functional_with_es_ssl/apps/ml/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/ml/index.ts @@ -12,7 +12,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); describe('ML app', function () { - this.tags(['mlqa', 'skipFirefox']); + this.tags(['ml', 'skipFirefox']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/simple_down_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/simple_down_alert.ts index 425ce5a55524d..963acca117881 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/simple_down_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/simple_down_alert.ts @@ -107,7 +107,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { group: 'xpack.uptime.alerts.actionGroups.monitorStatus', params: { message: - 'Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} from {{state.observerLocation}} {{{state.statusMessage}}} The latest error message is {{{state.latestErrorMessage}}}', + 'Monitor {{context.monitorName}} with url {{{context.monitorUrl}}} from {{context.observerLocation}} {{{context.statusMessage}}} The latest error message is {{{context.latestErrorMessage}}}', }, id: 'my-slack1', }, diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts index 510e94cf95f0d..9e6919c7a00e6 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/_post_urls.ts @@ -19,7 +19,7 @@ export const PDF_PRESERVE_PIE_VISUALIZATION_6_3 = `/api/reporting/generate/print )}`; export const PDF_PRINT_PIE_VISUALIZATION_FILTER_AND_SAVED_SEARCH_6_3 = `/api/reporting/generate/printablePdf?jobParams=${encodeURIComponent( - `(browserTimezone:America/New_York,layout:(id:print),objectType:visualization,relativeUrls:!('/app/kibana#/visualize/edit/befdb6b0-3e59-11e8-9fc3-39e49624228e?_g=(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!'Mon+Apr+09+2018+17:56:08+GMT-0400!',mode:absolute,to:!'Wed+Apr+11+2018+17:56:08+GMT-0400!'))&_a=(filters:!!((!'$state!':(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!'!'),uiState:(),vis:(aggs:!!((enabled:!!t,id:!'1!',params:(),schema:metric,type:count),(enabled:!!t,id:!'2!',params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!'1!',otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!'Filter+Test:+animals:+linked+to+search+with+filter!',type:pie))'),title:'Filter Test: animals: linked to search with filter')` + `(browserTimezone:America/New_York,layout:(dimensions:(height:588,width:1038),id:preserve_layout),objectType:visualization,relativeUrls:!('/app/kibana#/visualize/edit/befdb6b0-3e59-11e8-9fc3-39e49624228e?_g=(refreshInterval:(display:Off,pause:!!f,value:0),time:(from:!'Mon+Apr+09+2018+17:56:08+GMT-0400!',mode:absolute,to:!'Wed+Apr+11+2018+17:56:08+GMT-0400!'))&_a=(filters:!!((!'$state!':(store:appState),meta:(alias:!!n,disabled:!!f,index:a0f483a0-3dc9-11e8-8660-4d65aa086b3c,key:animal.keyword,negate:!!f,params:(query:dog,type:phrase),type:phrase,value:dog),query:(match:(animal.keyword:(query:dog,type:phrase))))),linked:!!t,query:(language:lucene,query:!'!'),uiState:(),vis:(aggs:!!((enabled:!!t,id:!'1!',params:(),schema:metric,type:count),(enabled:!!t,id:!'2!',params:(field:name.keyword,missingBucket:!!f,missingBucketLabel:Missing,order:desc,orderBy:!'1!',otherBucket:!!f,otherBucketLabel:Other,size:5),schema:segment,type:terms)),params:(addLegend:!!t,addTooltip:!!t,isDonut:!!t,labels:(last_level:!!t,show:!!f,truncate:100,values:!!t),legendPosition:right,type:pie),title:!'Filter+Test:+animals:+linked+to+search+with+filter!',type:pie))'),title:'Filter Test: animals: linked to search with filter')` )}`; export const JOB_PARAMS_CSV_DEFAULT_SPACE = `/api/reporting/generate/csv_searchsource?jobParams=${encodeURIComponent( diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts index 086f3373e2c71..e702be05f9bd8 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/new_jobs.ts @@ -74,15 +74,15 @@ export default function ({ getService }: FtrProviderContext) { const usage = await usageAPI.getUsageStats(); reportingAPI.expectRecentPdfAppStats(usage, 'visualization', 1); reportingAPI.expectRecentPdfAppStats(usage, 'dashboard', 1); - reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 0); - reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 2); + reportingAPI.expectRecentPdfLayoutStats(usage, 'preserve_layout', 1); + reportingAPI.expectRecentPdfLayoutStats(usage, 'print', 1); reportingAPI.expectRecentJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); reportingAPI.expectAllTimePdfAppStats(usage, 'visualization', 1); reportingAPI.expectAllTimePdfAppStats(usage, 'dashboard', 1); - reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 0); - reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 2); + reportingAPI.expectAllTimePdfLayoutStats(usage, 'preserve_layout', 1); + reportingAPI.expectAllTimePdfLayoutStats(usage, 'print', 1); reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'csv_searchsource', 0); reportingAPI.expectAllTimeJobTypeTotalStats(usage, 'printable_pdf', 2); }); diff --git a/x-pack/test/reporting_api_integration/services/usage.ts b/x-pack/test/reporting_api_integration/services/usage.ts index fd16f3859fa11..80204875cd6d6 100644 --- a/x-pack/test/reporting_api_integration/services/usage.ts +++ b/x-pack/test/reporting_api_integration/services/usage.ts @@ -101,31 +101,47 @@ export function createUsageServices({ getService }: FtrProviderContext) { }, expectRecentPdfAppStats(stats: UsageStats, app: string, count: number) { - expect( - stats.reporting.last_7_days.printable_pdf.app![app as keyof AvailableTotal['app']] - ).to.be(count); + const actual = + stats.reporting.last_7_days.printable_pdf.app![app as keyof AvailableTotal['app']]; + log.info(`expecting recent ${app} stats to have ${count} printable pdfs (actual: ${actual})`); + expect(actual).to.be(count); }, expectAllTimePdfAppStats(stats: UsageStats, app: string, count: number) { - expect(stats.reporting.printable_pdf.app![app as keyof AvailableTotal['app']]).to.be(count); + const actual = stats.reporting.printable_pdf.app![app as keyof AvailableTotal['app']]; + log.info( + `expecting all time pdf ${app} stats to have ${count} printable pdfs (actual: ${actual})` + ); + expect(actual).to.be(count); }, expectRecentPdfLayoutStats(stats: UsageStats, layout: string, count: number) { - expect(stats.reporting.last_7_days.printable_pdf.layout![layout as keyof LayoutCounts]).to.be( - count - ); + const actual = + stats.reporting.last_7_days.printable_pdf.layout![layout as keyof LayoutCounts]; + log.info(`expecting recent stats to report ${count} ${layout} layouts (actual: ${actual})`); + expect(actual).to.be(count); }, expectAllTimePdfLayoutStats(stats: UsageStats, layout: string, count: number) { - expect(stats.reporting.printable_pdf.layout![layout as keyof LayoutCounts]).to.be(count); + const actual = stats.reporting.printable_pdf.layout![layout as keyof LayoutCounts]; + log.info(`expecting all time stats to report ${count} ${layout} layouts (actual: ${actual})`); + expect(actual).to.be(count); }, expectRecentJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) { - expect(stats.reporting.last_7_days[jobType as keyof JobTypes].total).to.be(count); + const actual = stats.reporting.last_7_days[jobType as keyof JobTypes].total; + log.info( + `expecting recent stats to report ${count} ${jobType} job types (actual: ${actual})` + ); + expect(actual).to.be(count); }, expectAllTimeJobTypeTotalStats(stats: UsageStats, jobType: string, count: number) { - expect(stats.reporting[jobType as keyof JobTypes].total).to.be(count); + const actual = stats.reporting[jobType as keyof JobTypes].total; + log.info( + `expecting all time stats to report ${count} ${jobType} job types (actual: ${actual})` + ); + expect(actual).to.be(count); }, getCompletedReportCount(stats: UsageStats) { diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/index.ts b/x-pack/test/screenshot_creation/apps/ml_docs/index.ts index 9a12153682618..82460db174add 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/index.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning docs', function () { - this.tags(['mlqa']); + this.tags(['ml']); before(async () => { await ml.testResources.installAllKibanaSampleData(); diff --git a/yarn.lock b/yarn.lock index 45307af4aa044..ea3dec235c93e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3204,6 +3204,10 @@ version "0.0.0" uid "" +"@kbn/shared-ux-avatar-solution@link:bazel-bin/packages/shared-ux/avatar/solution": + version "0.0.0" + uid "" + "@kbn/shared-ux-button-exit-full-screen@link:bazel-bin/packages/shared-ux/button/exit_full_screen": version "0.0.0" uid "" @@ -3212,6 +3216,14 @@ version "0.0.0" uid "" +"@kbn/shared-ux-link-redirect-app@link:bazel-bin/packages/shared-ux/link/redirect_app": + version "0.0.0" + uid "" + +"@kbn/shared-ux-page-analytics-no-data@link:bazel-bin/packages/shared-ux/page/analytics_no_data": + version "0.0.0" + uid "" + "@kbn/shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services": version "0.0.0" uid "" @@ -6292,6 +6304,10 @@ version "0.0.0" uid "" +"@types/kbn__shared-ux-avatar-solution@link:bazel-bin/packages/shared-ux/avatar/solution/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__shared-ux-button-exit-full-screen@link:bazel-bin/packages/shared-ux/button/exit_full_screen/npm_module_types": version "0.0.0" uid "" @@ -6300,6 +6316,14 @@ version "0.0.0" uid "" +"@types/kbn__shared-ux-link-redirect-app@link:bazel-bin/packages/shared-ux/link/redirect_app/npm_module_types": + version "0.0.0" + uid "" + +"@types/kbn__shared-ux-page-analytics-no-data@link:bazel-bin/packages/shared-ux/page/analytics_no_data/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types": version "0.0.0" uid "" @@ -22178,6 +22202,13 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pdfjs-dist@^2.13.216: + version "2.13.216" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.13.216.tgz#251a11c9c8c6db19baacd833a4e6986c517d1ab3" + integrity sha512-qn/9a/3IHIKZarTK6ajeeFXBkG15Lg1Fx99PxU09PAU2i874X8mTcHJYyDJxu7WDfNhV6hM7bRQBZU384anoqQ== + dependencies: + web-streams-polyfill "^3.2.0" + pdfmake@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.2.4.tgz#7d58d64b59f8e9b9ed0b2494b17a9d94c575825b" @@ -29733,6 +29764,11 @@ web-streams-polyfill@^3.0.0: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.0.1.tgz#1f836eea307e8f4af15758ee473c7af755eb879e" integrity sha512-M+EmTdszMWINywOZaqpZ6VIEDUmNpRaTOuizF0ZKPjSDC8paMRe/jBBwFv0Yeyn5WYnM5pMqMQa82vpaE+IJRw== +web-streams-polyfill@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965" + integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"