diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 40be42461c4930..5340b4bf578cdf 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 f1552334406539..e55866fdae0216 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 3994cc75be33a0..7b4e0f82fc6ced 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 7441621f441f91..2cfd3169b45a30 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 5ddf45887a5308..ddce9feb3e640b 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 899a7843a68fc9..cf871abe6f18fe 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 50ca9fa91e0aab..eecaef06be453c 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 8ef3b23cd8c51d..da21aaf05b1396 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 f9248482279b8c..cf077a56ec417e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -105,7 +105,7 @@ pageLoadAssetSize: fieldFormats: 65209 kibanaReact: 74422 share: 71239 - uiActions: 35121 + uiActions: 35121 embeddable: 87309 embeddableEnhanced: 22107 uiActionsEnhanced: 38494 @@ -129,3 +129,4 @@ pageLoadAssetSize: screenshotting: 22870 synthetics: 40958 expressionXY: 29000 + kibanaUsageCollection: 16463 diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 184c16f96167f7..f2d5a60cd325e6 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -19305,7 +19305,7 @@ cmdShim.ifExists = cmdShimIfExists var fs = __webpack_require__("../../node_modules/graceful-fs/graceful-fs.js") -var mkdir = __webpack_require__("../../node_modules/cmd-shim/node_modules/mkdirp/index.js") +var mkdir = __webpack_require__("../../node_modules/mkdirp/index.js") , path = __webpack_require__("path") , toBatchSyntax = __webpack_require__("../../node_modules/cmd-shim/lib/to-batch-syntax.js") , shebangExpr = /^#\!\s*(?:\/usr\/bin\/env)?\s*([^ \t]+=[^ \t]+\s+)*\s*([^ \t]+)(.*)$/ @@ -19598,112 +19598,6 @@ function replaceDollarWithPercentPair(value) { -/***/ }), - -/***/ "../../node_modules/cmd-shim/node_modules/mkdirp/index.js": -/***/ (function(module, exports, __webpack_require__) { - -var path = __webpack_require__("path"); -var fs = __webpack_require__("fs"); -var _0777 = parseInt('0777', 8); - -module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; - -function mkdirP (p, opts, f, made) { - if (typeof opts === 'function') { - f = opts; - opts = {}; - } - else if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 & (~process.umask()); - } - if (!made) made = null; - - var cb = f || function () {}; - p = path.resolve(p); - - xfs.mkdir(p, mode, function (er) { - if (!er) { - made = made || p; - return cb(null, made); - } - switch (er.code) { - case 'ENOENT': - if (path.dirname(p) === p) return cb(er); - mkdirP(path.dirname(p), opts, function (er, made) { - if (er) cb(er, made); - else mkdirP(p, opts, cb, made); - }); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - xfs.stat(p, function (er2, stat) { - // if the stat fails, then that's super weird. - // let the original error be the failure reason. - if (er2 || !stat.isDirectory()) cb(er, made) - else cb(null, made); - }); - break; - } - }); -} - -mkdirP.sync = function sync (p, opts, made) { - if (!opts || typeof opts !== 'object') { - opts = { mode: opts }; - } - - var mode = opts.mode; - var xfs = opts.fs || fs; - - if (mode === undefined) { - mode = _0777 & (~process.umask()); - } - if (!made) made = null; - - p = path.resolve(p); - - try { - xfs.mkdirSync(p, mode); - made = made || p; - } - catch (err0) { - switch (err0.code) { - case 'ENOENT' : - made = sync(path.dirname(p), opts, made); - sync(p, opts, made); - break; - - // In the case of any other error, just see if there's a dir - // there already. If so, then hooray! If not, then something - // is borked. - default: - var stat; - try { - stat = xfs.statSync(p); - } - catch (err1) { - throw err0; - } - if (!stat.isDirectory()) throw err0; - break; - } - } - - return made; -}; - - /***/ }), /***/ "../../node_modules/color-convert/conversions.js": @@ -36304,6 +36198,112 @@ function isConstructorOrProto (obj, key) { } +/***/ }), + +/***/ "../../node_modules/mkdirp/index.js": +/***/ (function(module, exports, __webpack_require__) { + +var path = __webpack_require__("path"); +var fs = __webpack_require__("fs"); +var _0777 = parseInt('0777', 8); + +module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP; + +function mkdirP (p, opts, f, made) { + if (typeof opts === 'function') { + f = opts; + opts = {}; + } + else if (!opts || typeof opts !== 'object') { + opts = { mode: opts }; + } + + var mode = opts.mode; + var xfs = opts.fs || fs; + + if (mode === undefined) { + mode = _0777 & (~process.umask()); + } + if (!made) made = null; + + var cb = f || function () {}; + p = path.resolve(p); + + xfs.mkdir(p, mode, function (er) { + if (!er) { + made = made || p; + return cb(null, made); + } + switch (er.code) { + case 'ENOENT': + if (path.dirname(p) === p) return cb(er); + mkdirP(path.dirname(p), opts, function (er, made) { + if (er) cb(er, made); + else mkdirP(p, opts, cb, made); + }); + break; + + // In the case of any other error, just see if there's a dir + // there already. If so, then hooray! If not, then something + // is borked. + default: + xfs.stat(p, function (er2, stat) { + // if the stat fails, then that's super weird. + // let the original error be the failure reason. + if (er2 || !stat.isDirectory()) cb(er, made) + else cb(null, made); + }); + break; + } + }); +} + +mkdirP.sync = function sync (p, opts, made) { + if (!opts || typeof opts !== 'object') { + opts = { mode: opts }; + } + + var mode = opts.mode; + var xfs = opts.fs || fs; + + if (mode === undefined) { + mode = _0777 & (~process.umask()); + } + if (!made) made = null; + + p = path.resolve(p); + + try { + xfs.mkdirSync(p, mode); + made = made || p; + } + catch (err0) { + switch (err0.code) { + case 'ENOENT' : + made = sync(path.dirname(p), opts, made); + sync(p, opts, made); + break; + + // In the case of any other error, just see if there's a dir + // there already. If so, then hooray! If not, then something + // is borked. + default: + var stat; + try { + stat = xfs.statSync(p); + } + catch (err1) { + throw err0; + } + if (!stat.isDirectory()) throw err0; + break; + } + } + + return made; +}; + + /***/ }), /***/ "../../node_modules/multimatch/index.js": diff --git a/packages/kbn-shared-ux-components/BUILD.bazel b/packages/kbn-shared-ux-components/BUILD.bazel index 8eca4da0144933..b1420f53760419 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 05afc94f782c87..77586e8592b6a8 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 66b085b284391e..0046e9c3fd3c1d 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 79c0ea245b6cbd..b15f254a5274aa 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 f25edb069c6293..367fcd10b96a92 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 0bca3929f4c2d4..7b046bbe3fe8c2 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 77c41cddde6dac..84cbfb1c73a949 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 42d42dd805650f..3702dd4a456a71 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 f16f87039a6264..837eb5282507fa 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 fce0e996d99cd8..069192708e47b2 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 db2990726dc932..00000000000000 --- 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 db7462d7cb1bf0..00000000000000 --- 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 0023182940ae97..00000000000000 --- 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 0ca0e2a8d99780..00000000000000 --- 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 d36bace70b7c8c..00000000000000 --- 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 e1d0bd4bed653e..00000000000000 --- 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 bc26806016df0d..00000000000000 --- 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 deb71affc9c1a9..00000000000000 --- 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 c3b7dc63bce947..8091bd222d1a32 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 2a544445fc474c..1b16526bc8be85 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-jest-helpers/BUILD.bazel b/packages/kbn-test-jest-helpers/BUILD.bazel index dc8b83495494cb..85192829003e4f 100644 --- a/packages/kbn-test-jest-helpers/BUILD.bazel +++ b/packages/kbn-test-jest-helpers/BUILD.bazel @@ -60,7 +60,6 @@ RUNTIME_DEPS = [ "@npm//joi", "@npm//mustache", "@npm//normalize-path", - "@npm//parse-link-header", "@npm//prettier", "@npm//react", "@npm//react-dom", @@ -106,7 +105,6 @@ TYPES_DEPS = [ "@npm//@types/mustache", "@npm//@types/normalize-path", "@npm//@types/node", - "@npm//@types/parse-link-header", "@npm//@types/prettier", "@npm//@types/react", "@npm//@types/react-dom", diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index f7599e6d816498..15487aa781b8da 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -67,7 +67,6 @@ RUNTIME_DEPS = [ "@npm//js-yaml", "@npm//mustache", "@npm//normalize-path", - "@npm//parse-link-header", "@npm//prettier", "@npm//react-dom", "@npm//react-redux", @@ -115,7 +114,6 @@ TYPES_DEPS = [ "@npm//@types/mustache", "@npm//@types/normalize-path", "@npm//@types/node", - "@npm//@types/parse-link-header", "@npm//@types/prettier", "@npm//@types/react-dom", "@npm//@types/react-redux", diff --git a/packages/kbn-test/src/es/test_es_cluster.ts b/packages/kbn-test/src/es/test_es_cluster.ts index 42dc19445c2931..c065cb01a4c364 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 4159533e628bca..f71e4ac7d6ccd1 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 96ebcd79c4e436..506b6f139f7364 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 0ceba511f9b9bf..9de6500a453234 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 24702d699064cd..49a6ef16d6685e 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 89f0ea088cac87..12840b77dd8d92 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 bf2cc431595269..2726192328bda0 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 37cd708de1e00e..75a654fdfc5135 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 adbb18b5312d0c..2ee9de4053fef9 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 4c4a7128a05a9c..b9945adbdfb560 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 47d0b1c93b620b..b5026d397139d8 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 dd9fe4c93016c4..33a49ae2c80d1a 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 00000000000000..a253153cb92273 --- /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 00000000000000..841274441f6edb --- /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 00000000000000..6ca49f67e1dd55 --- /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 00000000000000..b0ec8ec947b09a --- /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 9817d7cdd8d45a..f0666987e0f79a 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 00000000000000..c2c9613bab87df --- /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 00000000000000..b47ff7c837f241 --- /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 7a8b20c3f8d648..ab7c675b24e0d4 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 00000000000000..0c38652a273953 --- /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 00000000000000..bf0ad682e30067 --- /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 00000000000000..93076efae5d7ca --- /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 00000000000000..861b9aa277db9f --- /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 00000000000000..8e2eada760ea2a --- /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 00000000000000..5f564a9709d0cf --- /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 00000000000000..6deb187dcec2a9 --- /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 dd26443eed171d..c46b93bb67aafd 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 00000000000000..8c94aa0033f2b0 --- /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 00000000000000..5efb99cc486649 --- /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 00000000000000..477471fe71824c --- /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 00000000000000..9bb3d0d9782d49 --- /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 00000000000000..1bb3875aec7aed --- /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 00000000000000..1e805ad4475b60 --- /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 00000000000000..22bc5a5cd0c55e --- /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 efc597cbdcb13e..2c27ccde84d67e 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 00000000000000..93076efae5d7ca --- /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 00000000000000..ad687fe8a220b1 --- /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 00000000000000..ab8cf8d1cb063b --- /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 00000000000000..76067f82881f77 --- /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 00000000000000..e9977444fb94e1 --- /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 00000000000000..be6fd3c45744e2 --- /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 00000000000000..0f187101979917 --- /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 00000000000000..31051328641f4f --- /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 00000000000000..8471cdf9546d2b --- /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 00000000000000..e091cac70d32bc --- /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 00000000000000..141f607a6257e6 --- /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 00000000000000..7b87084f745ef3 --- /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 00000000000000..3d690e56e0d23d --- /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 00000000000000..70ba29ed2f6489 --- /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 00000000000000..573ad073251009 --- /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 7c75470b890aa3..5d831a5bb8f788 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 281e3a0a4f3e03..e7f011cb4c5f20 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 9846e5f48dc217..cddd2f323f1fc9 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 88240429856d11..49967feb214d6f 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 264c6e52db0eb5..d8b604f00b46ec 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 35977d47aaaa72..32a551820a05b5 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 95f6a56ef68cb2..d171c48662cf6e 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 316428d46a957f..472e64e849b581 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 94068f2b64b125..63b04ed6f70b03 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 8aa2d6f1cfe551..ced601d0f39818 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 555df3c2c5c11e..0e67d787be1448 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 fd4674a7caf6eb..56673cda1a9536 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/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f3aedf36b66a78..c95a2308c39659 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 a451178cc46b08..cd9c41f392a0b0 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 00000000000000..9bd8bfc3a09448 --- /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 00000000000000..16c4dd85ea45ee --- /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 00000000000000..d7addc7afb261a --- /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 a103c888436646..50c40e4863beea 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 7095ad34cd1897..5cbbd30c79a242 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 4ea014457fd07b..08e830fba41555 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 39b55e5c6dd946..41fc5c6c37b783 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 00000000000000..d30aa0661e977f --- /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 00000000000000..24deee4afb5d0b --- /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 00000000000000..2bf67d02fe1104 --- /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 00000000000000..483e00d8d03fe1 --- /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 00000000000000..5474b8db0b27f2 --- /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 00000000000000..2b7a4b868b76ae --- /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 00000000000000..46762148a952cf --- /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 00000000000000..24deee4afb5d0b --- /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 00000000000000..ddeb85ee1be022 --- /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 00000000000000..ed2100dccf929b --- /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 7df27a3719e92a..a21b2b007f5e9e 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 a6604ac0bc1cd6..ef26492c2d6fd1 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 34bf0293113071..10f05ccbac945d 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 e57d6e25db8cd4..d9a1e648995bbb 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 db6cf1bc3d0068..5ae2a4498b55bf 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 62dc67a3ee941c..86c83a6b48be50 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 d8a0bffb4ed0bb..398578092425dd 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 6ba5e07ea873eb..f4467dfe35011f 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 f14f3452761e11..f371da244f871b 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 b947141a0c68ad..07e438ab521743 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 26727b56094a00..08f07e507d96b8 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 7a04b92c7e0637..3d41a9145dffa5 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 2b34aef33eeeeb..810d0a64d02514 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 de70e66fda5fcf..548d7f24d5da71 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 bb01338d8d5a06..0bff12ac787986 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 186c1f072aeddc..008c61b909ce12 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 7c2d0ebd1faad1..c7db17ea934d5e 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 7568bb9375fa6d..127aa804f77f8e 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 c4e54995b5979e..c73aa258863ed3 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 1072a684eeaad8..36d06d1cb9c7f7 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 a684e5ba928a83..8c5abc1bf4c2c0 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 29cf59f41a8712..fa0fc9e826e371 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 58d8de723639d1..eb6c8fb0888f25 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 85dadcfa8d7d26..5eb27325e535cc 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 2d87c0575845fb..d295be040db7a9 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 b3dbe4801f5443..ec855d98e7144e 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 7ed9bfb3f22942..5443302bce467b 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 f0a3502fd6813f..22e12d5ee11b59 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 5eac485e24c7b8..96b220283b4524 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 543e6ef6f4871e..43096d3de061c7 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 00000000000000..7366bb3fceebb6 --- /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 00000000000000..a9f4a6565c318b --- /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 5e83c33717abd1..ff1c00b56d0311 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 faee469d1c4bc5..0a34e756e37a63 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 63a2ea794e065a..b0f00ad202c5f3 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 dee4d424c84def..b689746a7af001 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 d817dc9d9ac0f6..f708d982822528 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 b5d3cee05ced68..0c222296928427 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 faae6450c52381..334b974c06108d 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 018f591fef79c8..68c2a0ecb7d885 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 87b4a1bda7ff7e..5c5f87b19f9774 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 cf8abc2fe9e161..758a34113efcd1 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 e64e23f039f894..70753e37f8e8a8 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 89a246c5c62654..12c1af65f95555 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 525af7cf95103f..ed38478c3a3eee 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 a511c2dc9f3da9..60a97845312e8b 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 00000000000000..7650b0d942180a --- /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 5776a163fd6a3d..510be94ab1705a 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 5b777803552fb0..ca7293a8c99c96 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 75378cdc863780..2d9326cf6cbb1c 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 e5a55322a2f105..adf791e8d2f481 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 876cb63b0333d0..e3c879d864a468 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 084bd65b70d31f..eebdf04337f698 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 bd666f52bf0bc5..54a24aac269b59 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 00000000000000..bd1fc948eb9372 --- /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 00000000000000..d957b9cafd4be1 --- /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 00000000000000..87f0931f1a3dbd --- /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 e00581833f621c..20def97df7aed1 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 4c4ca64f7ac075..edbf4df979f7b7 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 5e9662c5436418..b5b232aeeaae6a 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 5d5f4223fab9ae..57cc09dec4b169 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 7bd1a1a221edd5..9b155e5f7696c7 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 00000000000000..e68a2920155a58 --- /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 00000000000000..2621683b85d976 --- /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 00000000000000..3268982b69b9a1 --- /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 00000000000000..54b65d4c1c5276 --- /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 00000000000000..f65fdaddc45488 --- /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 00000000000000..da4905c044fe02 --- /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 6996c4885d25dc..df2fafa2a952cf 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 8bee3f273e1072..11e0eec3f08f0b 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 fe675302a982fd..90c37c6f910c94 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 8eb50a57fb858c..61bb1ba804a5ac 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 c8bf24156f129e..941818699ede20 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 60216650062c05..eb4798efc36cc9 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/osquery/cypress/fixtures/saved_objects/saved_query.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson index b29c4e28e731d6..0e1a2f4b67caca 100644 --- a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson +++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson @@ -14,6 +14,7 @@ "id": "Saved-Query-Id", "interval": "3600", "query": "select * from uptime;", + "platform": "linux,darwin", "updated_at": "2021-12-21T08:54:38.648Z", "updated_by": "elastic" }, diff --git a/x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts similarity index 59% rename from x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts rename to x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts index 1ce25a77f834a4..6dde0013a4bc69 100644 --- a/x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts +++ b/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts @@ -10,7 +10,7 @@ import { login } from '../../tasks/login'; import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver'; import { ROLES } from '../../test'; -describe('ALL - Delete ECS Mappings', () => { +describe('ALL - Edit saved query', () => { const SAVED_QUERY_ID = 'Saved-Query-Id'; before(() => { @@ -25,7 +25,7 @@ describe('ALL - Delete ECS Mappings', () => { runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query'); }); - it('to click the edit button and edit pack', () => { + it('by changing ecs mappings and platforms', () => { cy.react('CustomItemAction', { props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } }, }).click(); @@ -35,7 +35,33 @@ describe('ALL - Delete ECS Mappings', () => { .parents('[data-test-subj="ECSMappingEditorForm"]') .react('EuiButtonIcon', { props: { iconType: 'trash' } }) .click(); + + cy.react('PlatformCheckBoxGroupField').within(() => { + cy.react('EuiCheckbox', { + props: { + id: 'linux', + checked: true, + }, + }).should('exist'); + cy.react('EuiCheckbox', { + props: { + id: 'darwin', + checked: true, + }, + }).should('exist'); + + cy.react('EuiCheckbox', { + props: { + id: 'windows', + checked: false, + }, + }).should('exist'); + }); + + cy.get('#windows').check({ force: true }); + cy.react('EuiButton').contains('Update query').click(); + cy.wait(5000); cy.react('CustomItemAction', { @@ -43,5 +69,27 @@ describe('ALL - Delete ECS Mappings', () => { }).click(); cy.contains('Custom key/value pairs').should('not.exist'); cy.contains('Hours of uptime').should('not.exist'); + + cy.react('PlatformCheckBoxGroupField').within(() => { + cy.react('EuiCheckbox', { + props: { + id: 'linux', + checked: true, + }, + }).should('exist'); + cy.react('EuiCheckbox', { + props: { + id: 'darwin', + checked: true, + }, + }).should('exist'); + + cy.react('EuiCheckbox', { + props: { + id: 'windows', + checked: true, + }, + }).should('exist'); + }); }); }); diff --git a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx index 6da252f78aedf8..1d0d9f28d097b6 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx @@ -56,13 +56,6 @@ export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryF defaultValue, serializer: (payload) => produce(payload, (draft) => { - // @ts-expect-error update types - if (draft.platform?.split(',').length === 3) { - // if all platforms are checked then use undefined - // @ts-expect-error update types - delete draft.platform; - } - if (isArray(draft.version)) { if (!draft.version.length) { // @ts-expect-error update types 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 fd09396d6c86d3..66f905bd07cb21 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 7d31cdc0c6b8cd..bfdc74aa43ba60 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 d74313fa5ace19..1a04574155c1e4 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 00000000000000..ddd85a50fc6a53 --- /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 00000000000000..616e5f753b233a --- /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 00000000000000..7034dac76cfca3 --- /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 6805996fb1a5ac..74a80cf10b58bf 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 f12f2205d3a578..1b7076d05e4782 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/test/fleet_api_integration/apis/mock_http_server.d.ts b/x-pack/plugins/screenshotting/server/constants.ts similarity index 71% rename from x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts rename to x-pack/plugins/screenshotting/server/constants.ts index ac8e88b6fefe3b..38fde163778ece 100644 --- a/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts +++ b/x-pack/plugins/screenshotting/server/constants.ts @@ -5,6 +5,6 @@ * 2.0. */ -// No types for mock-http-server available, but we don't need them. +import path from 'path'; -declare module 'mock-http-server'; +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 ce28c53bb5f888..716b2bd46352f9 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 3e44a53a7f3c09..03192aacd887f0 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 afd9e294e9ae0c..7dd964594ca53a 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 033fb24c80685b..64027ffbd3cf2a 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 00000000000000..026d62ada876c8 --- /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 f157649bbb8488..67cfbd111e7505 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 5048d3f0a3be66..d06014c82ecc78 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 00000000000000..d4a408313fc432 --- /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 51da2e72c3bbd0..fb91c358486d86 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 0f613aff8d4568..8cb29901abdad7 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 a6af9febe8b3e3..1427b2b3bf3880 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 1a56c575057f05..4327c5a69a949c 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 bdddd8ab462076..eba1fa8238d051 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 4d3a03852e49fd..f55e72180c5cf6 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 9bef65bd9dad69..52f0dbf5d906e5 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 c866ca4c76956d..5f0c8c07172bb3 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 2f67219ac1ae59..c4d02806b59137 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 0361e6408e43bd..f7584cb04320e0 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 2c1238028ccf5e..b9ab025ecc021c 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 16c49d7c3afcbb..068cdfd90b1ae0 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 eabfe42691e8dd..31d8c0577780c1 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 8381adce21d2c6..f370b258b482fb 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 eb4509850414bc..ad821a509b77b6 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 f2ec05b11f5eac..a93d44013708b2 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 84e7c0d68400c6..b9a90ee18038a1 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 d305dedea3e109..243749f6861065 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 af248af730eee0..456b0675eee874 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 31a5e98bf9f021..88f8b964eb590a 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 0a6fb24c88156f..127171eab0f4dc 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 51a89f224fb294..8a0ea7eb4f660c 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 f23d9cf64202b7..8a30c8bb80de3c 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 9b42d92c275e92..fb1ca2dcd650a0 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 dd87bbaa23fef3..3b96df93ad35da 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 4d64f02d2c14b5..4928b368a96b42 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 db9951f235dfc6..f52ac6e1615778 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 15997355a2ae24..231057a50ee1f3 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 63f90d5a55a1f8..fb2dc3ea5bd035 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 6a36bf756cf19d..c504811fcd0ce2 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 08f30f8df024e4..c77c4ff8d6451d 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 ddf0425fb53864..0381c46214669d 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 ba0d030cfcf6f9..c809d0ee5c20d9 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 78974ecf1e64ca..0740c365f02e2a 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 17c576281835a9..05e38d565e9697 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 c71f4a5789fd22..6e3e98171b1099 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 96c02f7827a587..2ee9d226596d8f 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 d927f93adeffd0..9078782e36f0b5 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 1e6e020aff69c6..7920cf9721d47d 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 4b593aacbebf11..ed9f63be66dd4b 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 b290789419ed88..93ec331230a8a0 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 a1127c0e71c77c..0b206bfc450f31 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 783312b0d8608b..dcb47b205bb1b9 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 af2573e21f93de..0d04bb2ff70645 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 72dbac602cf8f1..7d9c528d763d74 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 e698dd270e1a87..cb21f8de77bd25 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 2afa284fcc3d75..4cdea1a726fe9f 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 b970a0efe56023..809ebf204e2a7e 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 0cf7c4177f057f..2ba4ac6f08350d 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 cfba10c25b17b3..67550ae17a4b06 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 82f76e66b4ebd5..3a33c95edba423 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 00000000000000..e82782f89973e0 --- /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 7129f3e24d4f1e..19844632cc4115 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 e9146ce5484223..947cd82cdd3423 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 1e428531e6aa96..1dc431c74a97de 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 a0cbd123b51694..7a84c41aa4a661 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 6b09b35c610a07..e22c4908486d18 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 8d04c4897dab0c..2bddf0a7d95125 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 d927f93adeffd0..daad4e85a1f8be 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 ef15775f862046..5e529a3430606a 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 973ebf2bbe3ab3..a75fc8d0bf794e 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 4334e72e9a16ec..1f4c20ea6faa53 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 b3f0e9e175d7a5..c7e00f8ed5b548 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 6ddf3bba3a81f0..0017a71a086feb 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 cf9bd17f11b814..00000000000000 --- 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 23d7d6fe9e2b53..00000000000000 --- 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 4c4bedfeb9b768..00000000000000 --- 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 d927f93adeffd0..cc9fffd2c93f52 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 c632ae48b3f885..18a6e130daed0d 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 e85b95b274720d..8b28c9e6ccda44 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 4a1c108b2fa5a8..1974a48e778413 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 a18a6075055a63..301fc5102a94f1 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 d927f93adeffd0..33d37ecd714571 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 68981de99fc9a8..ef674c1744a511 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 a4c50549f5aed2..8f3c30a15e5433 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 ab0988c424761f..657eb86e20c199 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 00000000000000..3c4cbbc0677bea --- /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 5595486260deee..c20957beb1ea53 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 2cc271e67194e5..d81b5933d77df5 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 9ac25b7fc9483b..d3f7000918a8e5 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 00000000000000..9d0fe82b9158c1 --- /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 4ced89e35d6088..c43cf74e3048c8 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 212bb029b6e0bb..e2ba704f5e1093 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 4515715327e055..37f238dbeecc90 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 5563bb9043c7f6..e68502f4dab5ab 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 e760549b7a1516..317a71ae79a0bd 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 d0d07ff2002816..17a471848867e6 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 7722c32f798373..5ba0c4dbbaa57b 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 5ed22ad51ad9f4..536badeee56a64 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 651f52434e55f7..f4d7103db0a619 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 a2a135b8cef0cd..2fcdf957f8909d 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 0188aa0361d942..dbdab2cc0a4b24 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 c5aed361aba3ec..c4a7fad8224eae 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 c64f1514b7c45c..b05763cfcf0794 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 a175e10fb7d185..9aaf523de6638d 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 fac9e46dcb65bc..942416c73b357e 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 425ce5a55524db..963acca117881e 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 510e94cf95f0d9..9e6919c7a00e6e 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 086f3373e2c71f..e702be05f9bd82 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 fd16f3859fa115..80204875cd6d6f 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 9a121536826180..82460db174add2 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 439a288a9db598..ea3dec235c93e1 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 "" @@ -6660,11 +6684,6 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/parse-link-header@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a" - integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA== - "@types/parse5@*", "@types/parse5@^5.0.0": version "5.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" @@ -7096,13 +7115,6 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== -"@types/tar-fs@^1.16.1": - version "1.16.1" - resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.1.tgz#6e3fba276c173e365ae91e55f7b797a0e64298e5" - integrity sha512-uQQIaa8ukcKf/1yy2kzfP1PF+7jEZghFDKpDvgtsYo/mbqM1g4Qza1Y5oAw6kJMa7eLA/HkmxUsDqb2sWKVF9g== - dependencies: - "@types/node" "*" - "@types/tar@^4.0.5": version "4.0.5" resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.5.tgz#5f953f183e36a15c6ce3f336568f6051b7b183f3" @@ -8587,7 +8599,7 @@ async@^1.4.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.1.4, async@^2.6.2: +async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -9189,7 +9201,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== -body-parser@1.19.0, body-parser@^1.18.1, body-parser@^1.18.3: +body-parser@1.19.0, body-parser@^1.18.3: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -10833,16 +10845,6 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -connect@^3.4.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -12041,11 +12043,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -dashify@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea" - integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo= - data-uri-to-buffer@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz#18ae979a6a0ca994b0625853916d2662bbae0b1a" @@ -14434,7 +14431,7 @@ fbjs@^0.8.1, fbjs@^0.8.9: setimmediate "^1.0.5" ua-parser-js "^0.7.18" -fd-slicer@1.1.0, fd-slicer@~1.1.0: +fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= @@ -14586,7 +14583,7 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.1.2, finalhandler@~1.1.2: +finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== @@ -15384,7 +15381,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob-watcher@5.0.3, glob-watcher@^5.0.3: +glob-watcher@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626" integrity sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg== @@ -16408,7 +16405,7 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@1.7.2, http-errors@~1.7.0, http-errors@~1.7.2: +http-errors@1.7.2, http-errors@~1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== @@ -16579,11 +16576,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -idx@^2.5.6: - version "2.5.6" - resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.6.tgz#1f824595070100ae9ad585c86db08dc74f83a59d" - integrity sha512-WFXLF7JgPytbMgelpRY46nHz5tyDcedJ76pLV+RJWdb8h33bxFq4bdZau38DhNSzk5eVniBf1K3jwfK+Lb5nYA== - ieee754@^1.1.12, ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -16780,13 +16772,6 @@ inline-style-prefixer@^4.0.0: bowser "^1.7.3" css-in-js-utils "^2.0.0" -inline-style@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b" - integrity sha1-L6nPYkWWqBCTVbklCU4Ti71eops= - dependencies: - dashify "^0.1.0" - inquirer@^7.0.0, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" @@ -19234,7 +19219,7 @@ loader-utils@2.0.0, loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== @@ -20266,7 +20251,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -20368,13 +20353,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - mkdirp@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" @@ -20481,16 +20459,6 @@ mock-fs@^5.1.2: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== -mock-http-server@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mock-http-server/-/mock-http-server-1.3.0.tgz#d2c2ffe65f77d3a4da8302c91d3bf687e5b51519" - integrity sha512-WC1fQ4kfOiiRZZ6IEOispJcfvz66m7VVbVFmnWsv1pOwL3psqYyLQGjFXg//zjPeZ//y/rxa8e2eh1Bb58cN7g== - dependencies: - body-parser "^1.18.1" - connect "^3.4.0" - multiparty "^4.1.2" - underscore "^1.8.3" - module-deps@^6.2.3: version "6.2.3" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee" @@ -20642,16 +20610,6 @@ multimatch@^4.0.0: arrify "^2.0.1" minimatch "^3.0.4" -multiparty@^4.1.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13" - integrity sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA== - dependencies: - fd-slicer "1.1.0" - http-errors "~1.7.0" - safe-buffer "5.1.2" - uid-safe "2.1.5" - murmurhash-js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" @@ -22050,13 +22008,6 @@ parse-json@^5.0.0: json-parse-better-errors "^1.0.1" lines-and-columns "^1.1.6" -parse-link-header@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7" - integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc= - dependencies: - xtend "~4.0.1" - parse-ms@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" @@ -22232,7 +22183,7 @@ pathval@^1.1.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pbf@3.2.1, pbf@^3.0.5, pbf@^3.2.1: +pbf@3.2.1, pbf@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a" integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ== @@ -22251,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" @@ -22379,13 +22337,6 @@ pixelmatch@^4.0.2: dependencies: pngjs "^3.0.0" -pixelmatch@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.1.0.tgz#b640f0e5a03a09f235a4b818ef3b9b98d9d0b911" - integrity sha512-HqtgvuWN12tBzKJf7jYsc38Ha28Q2NYpmBL9WostEGgDHJqbTLkjydZXL1ZHM02ZnB+Dkwlxo87HBY38kMiD6A== - dependencies: - pngjs "^3.4.0" - pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -23603,11 +23554,6 @@ randexp@0.4.6: discontinuous-range "1.0.0" ret "~0.1.10" -random-bytes@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" - integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= - random-word-slugs@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/random-word-slugs/-/random-word-slugs-0.0.5.tgz#6ccd6c7ea320be9fbc19507f8c3a7d4a970ff61f" @@ -25618,16 +25564,6 @@ sass-loader@^10.2.0: schema-utils "^3.0.0" semver "^7.3.2" -sass-resources-loader@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.1.tgz#c8427f3760bf7992f24f27d3889a1c797e971d3a" - integrity sha512-UsjQWm01xglINC1kPidYwKOBBzOElVupm9RwtOkRlY0hPA4GKi2KFsn4BZypRD1kudaXgUnGnfbiVOE7c+ybAg== - dependencies: - async "^2.1.4" - chalk "^1.1.3" - glob "^7.1.1" - loader-utils "^1.0.4" - save-pixels@^2.3.2: version "2.3.4" resolved "https://registry.yarnpkg.com/save-pixels/-/save-pixels-2.3.4.tgz#49d349c06b8d7c0127dbf0da24b44aca5afb59fe" @@ -27478,11 +27414,6 @@ syntax-error@^1.1.1: dependencies: acorn-node "^1.2.0" -tabbable@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081" - integrity sha512-nOWwx35/JuDI4ONuF0ZTo6lYvI0fY0tZCH1ErzY2EXfu4az50ZyiUX8X073FLiZtmWUVlkRnuXsehjJgCw9tYg== - tabbable@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c" @@ -27567,16 +27498,6 @@ tar-fs@^2.0.0, tar-fs@^2.1.1: pump "^3.0.0" tar-stream "^2.1.4" -tar-fs@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" - integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.0.0" - tar-stream@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" @@ -28447,13 +28368,6 @@ uglify-to-browserify@~1.0.0: resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= -uid-safe@2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" - integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== - dependencies: - random-bytes "~1.0.0" - umd@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" @@ -28506,7 +28420,7 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -underscore@^1.13.1, underscore@^1.8.3: +underscore@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== @@ -29726,15 +29640,6 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== -vt-pbf@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.1.tgz#b0f627e39a10ce91d943b898ed2363d21899fb82" - integrity sha512-pHjWdrIoxurpmTcbfBWXaPwSmtPAHS105253P1qyEfSTV2HJddqjM+kIHquaT/L6lVJIk9ltTGc0IxR/G47hYA== - dependencies: - "@mapbox/point-geometry" "0.1.0" - "@mapbox/vector-tile" "^1.3.1" - pbf "^3.0.5" - vt-pbf@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac" @@ -29859,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"