diff --git a/package.json b/package.json index cf4582a4c7df35..6012dae1008228 100644 --- a/package.json +++ b/package.json @@ -214,7 +214,6 @@ "color": "1.0.3", "commander": "^4.1.1", "compare-versions": "3.5.1", - "concat-stream": "1.6.2", "constate": "^1.3.2", "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", @@ -292,7 +291,7 @@ "lz-string": "^1.4.4", "mapbox-gl-draw-rectangle-mode": "1.0.4", "maplibre-gl": "1.15.2", - "markdown-it": "^10.0.0", + "markdown-it": "^12.3.2", "md5": "^2.1.0", "mdast-util-to-hast": "10.0.1", "memoize-one": "^6.0.0", @@ -631,7 +630,7 @@ "@types/lodash": "^4.14.159", "@types/lru-cache": "^5.1.0", "@types/lz-string": "^1.3.34", - "@types/markdown-it": "^0.0.7", + "@types/markdown-it": "^12.2.3", "@types/md5": "^2.2.0", "@types/mime": "^2.0.1", "@types/mime-types": "^2.1.0", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index d390966a6a52f6..647d421819597c 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -10413,50 +10413,44 @@ const mimicFn = __webpack_require__(153); const calledFunctions = new WeakMap(); -const oneTime = (fn, options = {}) => { - if (typeof fn !== 'function') { +const onetime = (function_, options = {}) => { + if (typeof function_ !== 'function') { throw new TypeError('Expected a function'); } - let ret; - let isCalled = false; + let returnValue; let callCount = 0; - const functionName = fn.displayName || fn.name || ''; + const functionName = function_.displayName || function_.name || ''; - const onetime = function (...args) { + const onetime = function (...arguments_) { calledFunctions.set(onetime, ++callCount); - if (isCalled) { - if (options.throw === true) { - throw new Error(`Function \`${functionName}\` can only be called once`); - } - - return ret; + if (callCount === 1) { + returnValue = function_.apply(this, arguments_); + function_ = null; + } else if (options.throw === true) { + throw new Error(`Function \`${functionName}\` can only be called once`); } - isCalled = true; - ret = fn.apply(this, args); - fn = null; - - return ret; + return returnValue; }; - mimicFn(onetime, fn); + mimicFn(onetime, function_); calledFunctions.set(onetime, callCount); return onetime; }; -module.exports = oneTime; +module.exports = onetime; // TODO: Remove this for the next major release -module.exports.default = oneTime; +module.exports.default = onetime; -module.exports.callCount = fn => { - if (!calledFunctions.has(fn)) { - throw new Error(`The given function \`${fn.name}\` is not wrapped by the \`onetime\` package`); +module.exports.callCount = function_ => { + if (!calledFunctions.has(function_)) { + throw new Error(`The given function \`${function_.name}\` is not wrapped by the \`onetime\` package`); } - return calledFunctions.get(fn); + return calledFunctions.get(function_); }; @@ -11180,158 +11174,203 @@ module.exports = { // Note: since nyc uses this module to output coverage, any lines // that are in the direct sync flow of nyc's outputCoverage are // ignored, since we can never get coverage for them. -var assert = __webpack_require__(162) -var signals = __webpack_require__(163) - -var EE = __webpack_require__(164) +// grab a reference to node's real process object right away +var process = global.process + +const processOk = function (process) { + return process && + typeof process === 'object' && + typeof process.removeListener === 'function' && + typeof process.emit === 'function' && + typeof process.reallyExit === 'function' && + typeof process.listeners === 'function' && + typeof process.kill === 'function' && + typeof process.pid === 'number' && + typeof process.on === 'function' +} + +// some kind of non-node environment, just no-op /* istanbul ignore if */ -if (typeof EE !== 'function') { - EE = EE.EventEmitter -} - -var emitter -if (process.__signal_exit_emitter__) { - emitter = process.__signal_exit_emitter__ +if (!processOk(process)) { + module.exports = function () { + return function () {} + } } else { - emitter = process.__signal_exit_emitter__ = new EE() - emitter.count = 0 - emitter.emitted = {} -} - -// Because this emitter is a global, we have to check to see if a -// previous version of this library failed to enable infinite listeners. -// I know what you're about to say. But literally everything about -// signal-exit is a compromise with evil. Get used to it. -if (!emitter.infinite) { - emitter.setMaxListeners(Infinity) - emitter.infinite = true -} + var assert = __webpack_require__(162) + var signals = __webpack_require__(163) + var isWin = /^win/i.test(process.platform) -module.exports = function (cb, opts) { - assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler') + var EE = __webpack_require__(164) + /* istanbul ignore if */ + if (typeof EE !== 'function') { + EE = EE.EventEmitter + } - if (loaded === false) { - load() + var emitter + if (process.__signal_exit_emitter__) { + emitter = process.__signal_exit_emitter__ + } else { + emitter = process.__signal_exit_emitter__ = new EE() + emitter.count = 0 + emitter.emitted = {} } - var ev = 'exit' - if (opts && opts.alwaysLast) { - ev = 'afterexit' + // Because this emitter is a global, we have to check to see if a + // previous version of this library failed to enable infinite listeners. + // I know what you're about to say. But literally everything about + // signal-exit is a compromise with evil. Get used to it. + if (!emitter.infinite) { + emitter.setMaxListeners(Infinity) + emitter.infinite = true } - var remove = function () { - emitter.removeListener(ev, cb) - if (emitter.listeners('exit').length === 0 && - emitter.listeners('afterexit').length === 0) { - unload() + module.exports = function (cb, opts) { + /* istanbul ignore if */ + if (!processOk(global.process)) { + return function () {} } - } - emitter.on(ev, cb) + assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler') - return remove -} + if (loaded === false) { + load() + } -module.exports.unload = unload -function unload () { - if (!loaded) { - return - } - loaded = false + var ev = 'exit' + if (opts && opts.alwaysLast) { + ev = 'afterexit' + } - signals.forEach(function (sig) { - try { - process.removeListener(sig, sigListeners[sig]) - } catch (er) {} - }) - process.emit = originalProcessEmit - process.reallyExit = originalProcessReallyExit - emitter.count -= 1 -} + var remove = function () { + emitter.removeListener(ev, cb) + if (emitter.listeners('exit').length === 0 && + emitter.listeners('afterexit').length === 0) { + unload() + } + } + emitter.on(ev, cb) -function emit (event, code, signal) { - if (emitter.emitted[event]) { - return + return remove } - emitter.emitted[event] = true - emitter.emit(event, code, signal) -} - -// { : , ... } -var sigListeners = {} -signals.forEach(function (sig) { - sigListeners[sig] = function listener () { - // If there are no other listeners, an exit is coming! - // Simplest way: remove us and then re-send the signal. - // We know that this will kill the process, so we can - // safely emit now. - var listeners = process.listeners(sig) - if (listeners.length === emitter.count) { - unload() - emit('exit', null, sig) - /* istanbul ignore next */ - emit('afterexit', null, sig) - /* istanbul ignore next */ - process.kill(process.pid, sig) + + var unload = function unload () { + if (!loaded || !processOk(global.process)) { + return } - } -}) + loaded = false -module.exports.signals = function () { - return signals -} + signals.forEach(function (sig) { + try { + process.removeListener(sig, sigListeners[sig]) + } catch (er) {} + }) + process.emit = originalProcessEmit + process.reallyExit = originalProcessReallyExit + emitter.count -= 1 + } + module.exports.unload = unload -module.exports.load = load + var emit = function emit (event, code, signal) { + /* istanbul ignore if */ + if (emitter.emitted[event]) { + return + } + emitter.emitted[event] = true + emitter.emit(event, code, signal) + } -var loaded = false + // { : , ... } + var sigListeners = {} + signals.forEach(function (sig) { + sigListeners[sig] = function listener () { + /* istanbul ignore if */ + if (!processOk(global.process)) { + return + } + // If there are no other listeners, an exit is coming! + // Simplest way: remove us and then re-send the signal. + // We know that this will kill the process, so we can + // safely emit now. + var listeners = process.listeners(sig) + if (listeners.length === emitter.count) { + unload() + emit('exit', null, sig) + /* istanbul ignore next */ + emit('afterexit', null, sig) + /* istanbul ignore next */ + if (isWin && sig === 'SIGHUP') { + // "SIGHUP" throws an `ENOSYS` error on Windows, + // so use a supported signal instead + sig = 'SIGINT' + } + /* istanbul ignore next */ + process.kill(process.pid, sig) + } + } + }) -function load () { - if (loaded) { - return + module.exports.signals = function () { + return signals } - loaded = true - // This is the number of onSignalExit's that are in play. - // It's important so that we can count the correct number of - // listeners on signals, and don't wait for the other one to - // handle it instead of us. - emitter.count += 1 + var loaded = false - signals = signals.filter(function (sig) { - try { - process.on(sig, sigListeners[sig]) - return true - } catch (er) { - return false + var load = function load () { + if (loaded || !processOk(global.process)) { + return } - }) + loaded = true - process.emit = processEmit - process.reallyExit = processReallyExit -} + // This is the number of onSignalExit's that are in play. + // It's important so that we can count the correct number of + // listeners on signals, and don't wait for the other one to + // handle it instead of us. + emitter.count += 1 -var originalProcessReallyExit = process.reallyExit -function processReallyExit (code) { - process.exitCode = code || 0 - emit('exit', process.exitCode, null) - /* istanbul ignore next */ - emit('afterexit', process.exitCode, null) - /* istanbul ignore next */ - originalProcessReallyExit.call(process, process.exitCode) -} + signals = signals.filter(function (sig) { + try { + process.on(sig, sigListeners[sig]) + return true + } catch (er) { + return false + } + }) + + process.emit = processEmit + process.reallyExit = processReallyExit + } + module.exports.load = load -var originalProcessEmit = process.emit -function processEmit (ev, arg) { - if (ev === 'exit') { - if (arg !== undefined) { - process.exitCode = arg + var originalProcessReallyExit = process.reallyExit + var processReallyExit = function processReallyExit (code) { + /* istanbul ignore if */ + if (!processOk(global.process)) { + return } - var ret = originalProcessEmit.apply(this, arguments) + process.exitCode = code || /* istanbul ignore next */ 0 emit('exit', process.exitCode, null) /* istanbul ignore next */ emit('afterexit', process.exitCode, null) - return ret - } else { - return originalProcessEmit.apply(this, arguments) + /* istanbul ignore next */ + originalProcessReallyExit.call(process, process.exitCode) + } + + var originalProcessEmit = process.emit + var processEmit = function processEmit (ev, arg) { + if (ev === 'exit' && processOk(global.process)) { + /* istanbul ignore else */ + if (arg !== undefined) { + process.exitCode = arg + } + var ret = originalProcessEmit.apply(this, arguments) + /* istanbul ignore next */ + emit('exit', process.exitCode, null) + /* istanbul ignore next */ + emit('afterexit', process.exitCode, null) + /* istanbul ignore next */ + return ret + } else { + return originalProcessEmit.apply(this, arguments) + } } } @@ -13921,8 +13960,9 @@ var assert = __webpack_require__(162); var debug = __webpack_require__(204); // Create handlers that pass events from native requests +var events = ["abort", "aborted", "connect", "error", "socket", "timeout"]; var eventHandlers = Object.create(null); -["abort", "aborted", "connect", "error", "socket", "timeout"].forEach(function (event) { +events.forEach(function (event) { eventHandlers[event] = function (arg1, arg2, arg3) { this._redirectable.emit(event, arg1, arg2, arg3); }; @@ -13931,7 +13971,7 @@ var eventHandlers = Object.create(null); // Error types with codes var RedirectionError = createErrorType( "ERR_FR_REDIRECTION_FAILURE", - "" + "Redirected request failed" ); var TooManyRedirectsError = createErrorType( "ERR_FR_TOO_MANY_REDIRECTS", @@ -13975,6 +14015,11 @@ function RedirectableRequest(options, responseCallback) { } RedirectableRequest.prototype = Object.create(Writable.prototype); +RedirectableRequest.prototype.abort = function () { + abortRequest(this._currentRequest); + this.emit("abort"); +}; + // Writes buffered data to the current native request RedirectableRequest.prototype.write = function (data, encoding, callback) { // Writing is not allowed if end has been called @@ -14054,40 +14099,72 @@ RedirectableRequest.prototype.removeHeader = function (name) { // Global timeout for all underlying requests RedirectableRequest.prototype.setTimeout = function (msecs, callback) { + var self = this; + + // Destroys the socket on timeout + function destroyOnTimeout(socket) { + socket.setTimeout(msecs); + socket.removeListener("timeout", socket.destroy); + socket.addListener("timeout", socket.destroy); + } + + // Sets up a timer to trigger a timeout event + function startTimer(socket) { + if (self._timeout) { + clearTimeout(self._timeout); + } + self._timeout = setTimeout(function () { + self.emit("timeout"); + clearTimer(); + }, msecs); + destroyOnTimeout(socket); + } + + // Stops a timeout from triggering + function clearTimer() { + // Clear the timeout + if (self._timeout) { + clearTimeout(self._timeout); + self._timeout = null; + } + + // Clean up all attached listeners + self.removeListener("abort", clearTimer); + self.removeListener("error", clearTimer); + self.removeListener("response", clearTimer); + if (callback) { + self.removeListener("timeout", callback); + } + if (!self.socket) { + self._currentRequest.removeListener("socket", startTimer); + } + } + + // Attach callback if passed if (callback) { - this.once("timeout", callback); + this.on("timeout", callback); } + // Start the timer if or when the socket is opened if (this.socket) { - startTimer(this, msecs); + startTimer(this.socket); } else { - var self = this; - this._currentRequest.once("socket", function () { - startTimer(self, msecs); - }); + this._currentRequest.once("socket", startTimer); } - this.once("response", clearTimer); - this.once("error", clearTimer); + // Clean up on events + this.on("socket", destroyOnTimeout); + this.on("abort", clearTimer); + this.on("error", clearTimer); + this.on("response", clearTimer); return this; }; -function startTimer(request, msecs) { - clearTimeout(request._timeout); - request._timeout = setTimeout(function () { - request.emit("timeout"); - }, msecs); -} - -function clearTimer() { - clearTimeout(this._timeout); -} - // Proxy all other public ClientRequest methods [ - "abort", "flushHeaders", "getHeader", + "flushHeaders", "getHeader", "setNoDelay", "setSocketKeepAlive", ].forEach(function (method) { RedirectableRequest.prototype[method] = function (a, b) { @@ -14157,11 +14234,8 @@ RedirectableRequest.prototype._performRequest = function () { // Set up event handlers request._redirectable = this; - for (var event in eventHandlers) { - /* istanbul ignore else */ - if (event) { - request.on(event, eventHandlers[event]); - } + for (var e = 0; e < events.length; e++) { + request.on(events[e], eventHandlers[events[e]]); } // End a redirected request @@ -14215,86 +14289,101 @@ RedirectableRequest.prototype._processResponse = function (response) { // the user agent MAY automatically redirect its request to the URI // referenced by the Location field value, // even if the specific status code is not understood. + + // If the response is not a redirect; return it as-is var location = response.headers.location; - if (location && this._options.followRedirects !== false && - statusCode >= 300 && statusCode < 400) { - // Abort the current request - this._currentRequest.removeAllListeners(); - this._currentRequest.on("error", noop); - this._currentRequest.abort(); - // Discard the remainder of the response to avoid waiting for data - response.destroy(); - - // RFC7231§6.4: A client SHOULD detect and intervene - // in cyclical redirections (i.e., "infinite" redirection loops). - if (++this._redirectCount > this._options.maxRedirects) { - this.emit("error", new TooManyRedirectsError()); - return; - } + if (!location || this._options.followRedirects === false || + statusCode < 300 || statusCode >= 400) { + response.responseUrl = this._currentUrl; + response.redirects = this._redirects; + this.emit("response", response); - // RFC7231§6.4: Automatic redirection needs to done with - // care for methods not known to be safe, […] - // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change - // the request method from POST to GET for the subsequent request. - if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" || - // RFC7231§6.4.4: The 303 (See Other) status code indicates that - // the server is redirecting the user agent to a different resource […] - // A user agent can perform a retrieval request targeting that URI - // (a GET or HEAD request if using HTTP) […] - (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) { - this._options.method = "GET"; - // Drop a possible entity and headers related to it - this._requestBodyBuffers = []; - removeMatchingHeaders(/^content-/i, this._options.headers); - } - - // Drop the Host header, as the redirect might lead to a different host - var previousHostName = removeMatchingHeaders(/^host$/i, this._options.headers) || - url.parse(this._currentUrl).hostname; - - // Create the redirected request - var redirectUrl = url.resolve(this._currentUrl, location); - debug("redirecting to", redirectUrl); - this._isRedirect = true; - var redirectUrlParts = url.parse(redirectUrl); - Object.assign(this._options, redirectUrlParts); - - // Drop the Authorization header if redirecting to another host - if (redirectUrlParts.hostname !== previousHostName) { - removeMatchingHeaders(/^authorization$/i, this._options.headers); - } - - // Evaluate the beforeRedirect callback - if (typeof this._options.beforeRedirect === "function") { - var responseDetails = { headers: response.headers }; - try { - this._options.beforeRedirect.call(null, this._options, responseDetails); - } - catch (err) { - this.emit("error", err); - return; - } - this._sanitizeOptions(this._options); - } + // Clean up + this._requestBodyBuffers = []; + return; + } + + // The response is a redirect, so abort the current request + abortRequest(this._currentRequest); + // Discard the remainder of the response to avoid waiting for data + response.destroy(); - // Perform the redirected request + // RFC7231§6.4: A client SHOULD detect and intervene + // in cyclical redirections (i.e., "infinite" redirection loops). + if (++this._redirectCount > this._options.maxRedirects) { + this.emit("error", new TooManyRedirectsError()); + return; + } + + // RFC7231§6.4: Automatic redirection needs to done with + // care for methods not known to be safe, […] + // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change + // the request method from POST to GET for the subsequent request. + if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" || + // RFC7231§6.4.4: The 303 (See Other) status code indicates that + // the server is redirecting the user agent to a different resource […] + // A user agent can perform a retrieval request targeting that URI + // (a GET or HEAD request if using HTTP) […] + (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) { + this._options.method = "GET"; + // Drop a possible entity and headers related to it + this._requestBodyBuffers = []; + removeMatchingHeaders(/^content-/i, this._options.headers); + } + + // Drop the Host header, as the redirect might lead to a different host + var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers); + + // If the redirect is relative, carry over the host of the last request + var currentUrlParts = url.parse(this._currentUrl); + var currentHost = currentHostHeader || currentUrlParts.host; + var currentUrl = /^\w+:/.test(location) ? this._currentUrl : + url.format(Object.assign(currentUrlParts, { host: currentHost })); + + // Determine the URL of the redirection + var redirectUrl; + try { + redirectUrl = url.resolve(currentUrl, location); + } + catch (cause) { + this.emit("error", new RedirectionError(cause)); + return; + } + + // Create the redirected request + debug("redirecting to", redirectUrl); + this._isRedirect = true; + var redirectUrlParts = url.parse(redirectUrl); + Object.assign(this._options, redirectUrlParts); + + // Drop confidential headers when redirecting to a less secure protocol + // or to a different domain that is not a superdomain + if (redirectUrlParts.protocol !== currentUrlParts.protocol && + redirectUrlParts.protocol !== "https:" || + redirectUrlParts.host !== currentHost && + !isSubdomain(redirectUrlParts.host, currentHost)) { + removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); + } + + // Evaluate the beforeRedirect callback + if (typeof this._options.beforeRedirect === "function") { + var responseDetails = { headers: response.headers }; try { - this._performRequest(); + this._options.beforeRedirect.call(null, this._options, responseDetails); } - catch (cause) { - var error = new RedirectionError("Redirected request failed: " + cause.message); - error.cause = cause; - this.emit("error", error); + catch (err) { + this.emit("error", err); + return; } + this._sanitizeOptions(this._options); } - else { - // The response is not a redirect; return it as-is - response.responseUrl = this._currentUrl; - response.redirects = this._redirects; - this.emit("response", response); - // Clean up - this._requestBodyBuffers = []; + // Perform the redirected request + try { + this._performRequest(); + } + catch (cause) { + this.emit("error", new RedirectionError(cause)); } }; @@ -14314,7 +14403,7 @@ function wrap(protocols) { var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); // Executes a request, following redirects - wrappedProtocol.request = function (input, options, callback) { + function request(input, options, callback) { // Parse parameters if (typeof input === "string") { var urlStr = input; @@ -14349,14 +14438,20 @@ function wrap(protocols) { assert.equal(options.protocol, protocol, "protocol mismatch"); debug("options", options); return new RedirectableRequest(options, callback); - }; + } // Executes a GET request, following redirects - wrappedProtocol.get = function (input, options, callback) { - var request = wrappedProtocol.request(input, options, callback); - request.end(); - return request; - }; + function get(input, options, callback) { + var wrappedRequest = wrappedProtocol.request(input, options, callback); + wrappedRequest.end(); + return wrappedRequest; + } + + // Expose the properties on the wrapped protocol + Object.defineProperties(wrappedProtocol, { + request: { value: request, configurable: true, enumerable: true, writable: true }, + get: { value: get, configurable: true, enumerable: true, writable: true }, + }); }); return exports; } @@ -14392,13 +14487,20 @@ function removeMatchingHeaders(regex, headers) { delete headers[header]; } } - return lastValue; + return (lastValue === null || typeof lastValue === "undefined") ? + undefined : String(lastValue).trim(); } function createErrorType(code, defaultMessage) { - function CustomError(message) { + function CustomError(cause) { Error.captureStackTrace(this, this.constructor); - this.message = message || defaultMessage; + if (!cause) { + this.message = defaultMessage; + } + else { + this.message = defaultMessage + ": " + cause.message; + this.cause = cause; + } } CustomError.prototype = new Error(); CustomError.prototype.constructor = CustomError; @@ -14407,6 +14509,19 @@ function createErrorType(code, defaultMessage) { return CustomError; } +function abortRequest(request) { + for (var e = 0; e < events.length; e++) { + request.removeListener(events[e], eventHandlers[events[e]]); + } + request.on("error", noop); + request.abort(); +} + +function isSubdomain(subdomain, domain) { + const dot = subdomain.length - domain.length - 1; + return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain); +} + // Exports module.exports = wrap({ http: http, https: https }); module.exports.wrap = wrap; @@ -14423,14 +14538,20 @@ module.exports = require("url"); /***/ (function(module, exports, __webpack_require__) { var debug; -try { - /* eslint global-require: off */ - debug = __webpack_require__(205)("follow-redirects"); -} -catch (error) { - debug = function () { /* */ }; -} -module.exports = debug; + +module.exports = function () { + if (!debug) { + try { + /* eslint global-require: off */ + debug = __webpack_require__(205)("follow-redirects"); + } + catch (error) { /* */ } + if (typeof debug !== "function") { + debug = function () { /* */ }; + } + } + debug.apply(null, arguments); +}; /***/ }), @@ -18514,7 +18635,6 @@ function pauseStreams (streams, options) { module.exports = glob -var fs = __webpack_require__(132) var rp = __webpack_require__(245) var minimatch = __webpack_require__(247) var Minimatch = minimatch.Minimatch @@ -18525,8 +18645,6 @@ var assert = __webpack_require__(162) var isAbsolute = __webpack_require__(253) var globSync = __webpack_require__(254) var common = __webpack_require__(255) -var alphasort = common.alphasort -var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp var inflight = __webpack_require__(256) @@ -18977,7 +19095,7 @@ Glob.prototype._readdirInGlobStar = function (abs, cb) { var lstatcb = inflight(lstatkey, lstatcb_) if (lstatcb) - fs.lstat(abs, lstatcb) + self.fs.lstat(abs, lstatcb) function lstatcb_ (er, lstat) { if (er && er.code === 'ENOENT') @@ -19018,7 +19136,7 @@ Glob.prototype._readdir = function (abs, inGlobStar, cb) { } var self = this - fs.readdir(abs, readdirCb(this, abs, cb)) + self.fs.readdir(abs, readdirCb(this, abs, cb)) } function readdirCb (self, abs, cb) { @@ -19222,13 +19340,13 @@ Glob.prototype._stat = function (f, cb) { var self = this var statcb = inflight('stat\0' + abs, lstatcb_) if (statcb) - fs.lstat(abs, statcb) + self.fs.lstat(abs, statcb) function lstatcb_ (er, lstat) { if (lstat && lstat.isSymbolicLink()) { // If it's a symlink, then treat it as the target, unless // the target does not exist, then treat it as a file. - return fs.stat(abs, function (er, stat) { + return self.fs.stat(abs, function (er, stat) { if (er) self._stat2(f, abs, null, lstat, cb) else @@ -20948,7 +21066,6 @@ module.exports.win32 = win32; module.exports = globSync globSync.GlobSync = GlobSync -var fs = __webpack_require__(132) var rp = __webpack_require__(245) var minimatch = __webpack_require__(247) var Minimatch = minimatch.Minimatch @@ -20958,8 +21075,6 @@ var path = __webpack_require__(4) var assert = __webpack_require__(162) var isAbsolute = __webpack_require__(253) var common = __webpack_require__(255) -var alphasort = common.alphasort -var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp var childrenIgnored = common.childrenIgnored @@ -21195,7 +21310,7 @@ GlobSync.prototype._readdirInGlobStar = function (abs) { var lstat var stat try { - lstat = fs.lstatSync(abs) + lstat = this.fs.lstatSync(abs) } catch (er) { if (er.code === 'ENOENT') { // lstat failed, doesn't exist @@ -21232,7 +21347,7 @@ GlobSync.prototype._readdir = function (abs, inGlobStar) { } try { - return this._readdirEntries(abs, fs.readdirSync(abs)) + return this._readdirEntries(abs, this.fs.readdirSync(abs)) } catch (er) { this._readdirError(abs, er) return null @@ -21391,7 +21506,7 @@ GlobSync.prototype._stat = function (f) { if (!stat) { var lstat try { - lstat = fs.lstatSync(abs) + lstat = this.fs.lstatSync(abs) } catch (er) { if (er && (er.code === 'ENOENT' || er.code === 'ENOTDIR')) { this.statCache[abs] = false @@ -21401,7 +21516,7 @@ GlobSync.prototype._stat = function (f) { if (lstat && lstat.isSymbolicLink()) { try { - stat = fs.statSync(abs) + stat = this.fs.statSync(abs) } catch (er) { stat = lstat } @@ -21437,8 +21552,6 @@ GlobSync.prototype._makeAbs = function (f) { /* 255 */ /***/ (function(module, exports, __webpack_require__) { -exports.alphasort = alphasort -exports.alphasorti = alphasorti exports.setopts = setopts exports.ownProp = ownProp exports.makeAbs = makeAbs @@ -21451,17 +21564,14 @@ function ownProp (obj, field) { return Object.prototype.hasOwnProperty.call(obj, field) } +var fs = __webpack_require__(132) var path = __webpack_require__(4) var minimatch = __webpack_require__(247) var isAbsolute = __webpack_require__(253) var Minimatch = minimatch.Minimatch -function alphasorti (a, b) { - return a.toLowerCase().localeCompare(b.toLowerCase()) -} - function alphasort (a, b) { - return a.localeCompare(b) + return a.localeCompare(b, 'en') } function setupIgnores (self, options) { @@ -21520,6 +21630,7 @@ function setopts (self, pattern, options) { self.stat = !!options.stat self.noprocess = !!options.noprocess self.absolute = !!options.absolute + self.fs = options.fs || fs self.maxLength = options.maxLength || Infinity self.cache = options.cache || Object.create(null) @@ -21589,7 +21700,7 @@ function finish (self) { all = Object.keys(all) if (!self.nosort) - all = all.sort(self.nocase ? alphasorti : alphasort) + all = all.sort(alphasort) // at *some* point we statted all of these if (self.mark) { diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx index c1e026064fdfb1..0adb06d91d268c 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx @@ -202,9 +202,6 @@ export const HeatmapComponent: FC = memo( const cell = e[0][0]; const { x, y } = cell.datum; - const xAxisFieldName = xAxisColumn?.meta?.field; - const timeFieldName = isTimeBasedSwimLane ? xAxisFieldName : ''; - const points = [ { row: table.rows.findIndex((r) => r[xAxisColumn.id] === x), @@ -229,35 +226,21 @@ export const HeatmapComponent: FC = memo( value: point.value, table, })), - timeFieldName, }; onClickValue(context); }, - [ - isTimeBasedSwimLane, - onClickValue, - table, - xAxisColumn?.id, - xAxisColumn?.meta?.field, - xAxisColumnIndex, - yAxisColumn, - yAxisColumnIndex, - ] + [onClickValue, table, xAxisColumn?.id, xAxisColumnIndex, yAxisColumn, yAxisColumnIndex] ); const onBrushEnd = useCallback( (e: HeatmapBrushEvent) => { const { x, y } = e; - const xAxisFieldName = xAxisColumn?.meta?.field; - const timeFieldName = isTimeBasedSwimLane ? xAxisFieldName : ''; - if (isTimeBasedSwimLane) { const context: BrushEvent['data'] = { range: x as number[], table, column: xAxisColumnIndex, - timeFieldName, }; onSelectRange(context); } else { @@ -289,7 +272,6 @@ export const HeatmapComponent: FC = memo( value: point.value, table, })), - timeFieldName, }; onClickValue(context); } diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 28361114be6e17..fa9b7ac86a7fa6 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -54,7 +54,6 @@ import { KueryNode as oldKueryNode, FilterMeta as oldFilterMeta, FILTERS as oldFILTERS, - IFieldSubType as oldIFieldSubType, EsQueryConfig as oldEsQueryConfig, compareFilters as oldCompareFilters, COMPARE_ALL_OPTIONS as OLD_COMPARE_ALL_OPTIONS, @@ -356,12 +355,6 @@ type KueryNode = oldKueryNode; */ type FilterMeta = oldFilterMeta; -/** - * @deprecated Import from the "@kbn/es-query" package directly instead. - * @removeBy 8.1 - */ -type IFieldSubType = oldIFieldSubType; - /** * @deprecated Import from the "@kbn/es-query" package directly instead. * @removeBy 8.1 @@ -385,7 +378,6 @@ export type { RangeFilter, KueryNode, FilterMeta, - IFieldSubType, EsQueryConfig, }; export { diff --git a/src/plugins/data/common/kbn_field_types/index.ts b/src/plugins/data/common/kbn_field_types/index.ts index f01401948dec81..5c0c2102f804c6 100644 --- a/src/plugins/data/common/kbn_field_types/index.ts +++ b/src/plugins/data/common/kbn_field_types/index.ts @@ -7,29 +7,6 @@ */ // NOTE: trick to mark exports as deprecated (only for constants and types, but not for interfaces, classes or enums) -import { - getFilterableKbnTypeNames as oldGetFilterableKbnTypeNames, - getKbnFieldType as oldGetKbnFieldType, - getKbnTypeNames as oldGetKbnTypeNames, - KbnFieldType, -} from '@kbn/field-types'; +import { KbnFieldType } from '@kbn/field-types'; -/** - * @deprecated Import from the "@kbn/field-types" package directly instead. - * @removeBy 8.1 - */ -const getFilterableKbnTypeNames = oldGetFilterableKbnTypeNames; - -/** - * @deprecated Import from the "@kbn/field-types" package directly instead. - * @removeBy 8.1 - */ -const getKbnFieldType = oldGetKbnFieldType; - -/** - * @deprecated Import from the "@kbn/field-types" package directly instead. - * @removeBy 8.1 - */ -const getKbnTypeNames = oldGetKbnTypeNames; - -export { getKbnFieldType, getKbnTypeNames, getFilterableKbnTypeNames, KbnFieldType }; +export { KbnFieldType }; diff --git a/src/plugins/data/public/deprecated.ts b/src/plugins/data/public/deprecated.ts index 8b90f92b932e03..0458a940482de2 100644 --- a/src/plugins/data/public/deprecated.ts +++ b/src/plugins/data/public/deprecated.ts @@ -47,7 +47,6 @@ import { PhraseFilter, CustomFilter, MatchAllFilter, - IFieldSubType, EsQueryConfig, FilterStateStore, compareFilters, @@ -147,7 +146,6 @@ export type { PhraseFilter, CustomFilter, MatchAllFilter, - IFieldSubType, EsQueryConfig, }; export { isFilter, isFilters }; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 4b7b447d2c8be0..ce6f2e03744faa 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -292,7 +292,7 @@ export type { export type { AggsStart } from './search/aggs'; -export { getTime, getKbnTypeNames } from '../common'; +export { getTime } from '../common'; export { isTimeRange, isQuery } from '../common'; diff --git a/src/plugins/data/server/deprecated.ts b/src/plugins/data/server/deprecated.ts index f9f77ee0ca12f9..98db107f32a116 100644 --- a/src/plugins/data/server/deprecated.ts +++ b/src/plugins/data/server/deprecated.ts @@ -67,4 +67,4 @@ export const esQuery = { buildEsQuery, }; -export type { Filter, Query, EsQueryConfig, KueryNode, IFieldSubType } from '../common'; +export type { Filter, Query, EsQueryConfig, KueryNode } from '../common'; diff --git a/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx index e6e48c477ebc96..4bee00f3c4b2a1 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor_context.tsx @@ -8,6 +8,7 @@ import React, { createContext, useContext, FunctionComponent, useMemo } from 'react'; import { NotificationsStart, CoreStart } from 'src/core/public'; +import { FieldFormatsStart } from '../shared_imports'; import type { DataView, DataPublicPluginStart } from '../shared_imports'; import { ApiService } from '../lib/api'; import type { InternalFieldType, PluginStart } from '../types'; @@ -25,7 +26,7 @@ export interface Context { notifications: NotificationsStart; }; fieldFormatEditors: PluginStart['fieldFormatEditors']; - fieldFormats: DataPublicPluginStart['fieldFormats']; + fieldFormats: FieldFormatsStart; /** * An array of field names not allowed. * e.g we probably don't want a user to give a name of an existing diff --git a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx index 5b431424c1b448..bd4f62b2c55f33 100644 --- a/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -11,6 +11,7 @@ import { DocLinksStart, NotificationsStart, CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { DataViewField, DataView, @@ -51,7 +52,7 @@ export interface Props { apiService: ApiService; /** Field format */ fieldFormatEditors: PluginStart['fieldFormatEditors']; - fieldFormats: DataPublicPluginStart['fieldFormats']; + fieldFormats: FieldFormatsStart; uiSettings: CoreStart['uiSettings']; } diff --git a/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx index c55385e152bcfd..e921d0beafce16 100644 --- a/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx +++ b/src/plugins/data_view_field_editor/public/components/field_format_editor/field_format_editor.tsx @@ -11,24 +11,21 @@ import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { - IndexPattern, - KBN_FIELD_TYPES, - ES_FIELD_TYPES, - DataPublicPluginStart, -} from 'src/plugins/data/public'; +import { KBN_FIELD_TYPES, ES_FIELD_TYPES } from 'src/plugins/data/public'; import type { FieldFormatInstanceType } from 'src/plugins/field_formats/common'; import { CoreStart } from 'src/core/public'; import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; +import { DataView } from 'src/plugins/data_views/public'; import { FormatEditor } from './format_editor'; import { FormatEditorServiceStart } from '../../service'; import { FieldFormatConfig } from '../../types'; export interface FormatSelectEditorProps { esTypes: ES_FIELD_TYPES[]; - indexPattern: IndexPattern; + indexPattern: DataView; fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; - fieldFormats: DataPublicPluginStart['fieldFormats']; + fieldFormats: FieldFormatsStart; uiSettings: CoreStart['uiSettings']; onChange: (change?: FieldFormatConfig) => void; onError: (error?: string) => void; @@ -54,7 +51,7 @@ interface InitialFieldTypeFormat extends FieldTypeFormat { const getFieldTypeFormatsList = ( fieldType: KBN_FIELD_TYPES, defaultFieldFormat: FieldFormatInstanceType, - fieldFormats: DataPublicPluginStart['fieldFormats'] + fieldFormats: FieldFormatsStart ) => { const formatsByType = fieldFormats.getByFieldType(fieldType).map(({ id, title }) => ({ id, diff --git a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx index a4a09562c300f3..981654ac52d916 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx +++ b/src/plugins/data_view_field_editor/public/components/preview/field_preview_context.tsx @@ -16,6 +16,7 @@ import React, { useRef, FunctionComponent, } from 'react'; +import { renderToString } from 'react-dom/server'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; @@ -45,8 +46,10 @@ const defaultParams: Params = { format: null, }; -export const defaultValueFormatter = (value: unknown) => - `${typeof value === 'object' ? JSON.stringify(value) : value ?? '-'}`; +export const defaultValueFormatter = (value: unknown) => { + const content = typeof value === 'object' ? JSON.stringify(value) : String(value) ?? '-'; + return renderToString(<>{content}); +}; export const FieldPreviewProvider: FunctionComponent = ({ children }) => { const previewCount = useRef(0); diff --git a/src/plugins/data_view_field_editor/public/plugin.test.tsx b/src/plugins/data_view_field_editor/public/plugin.test.tsx index fe7e8c57cd4ece..eba7d19a5a0d05 100644 --- a/src/plugins/data_view_field_editor/public/plugin.test.tsx +++ b/src/plugins/data_view_field_editor/public/plugin.test.tsx @@ -20,6 +20,7 @@ jest.mock('../../kibana_react/public', () => { import { CoreStart } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; +import { fieldFormatsServiceMock } from '../../field_formats/public/mocks'; import { usageCollectionPluginMock } from '../../usage_collection/public/mocks'; import { FieldEditorLoader } from './components/field_editor_loader'; @@ -35,7 +36,7 @@ describe('DataViewFieldEditorPlugin', () => { data: dataPluginMock.createStartContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), dataViews: dataPluginMock.createStartContract().dataViews, - fieldFormats: dataPluginMock.createStartContract().fieldFormats, + fieldFormats: fieldFormatsServiceMock.createStartContract(), }; let plugin: IndexPatternFieldEditorPlugin; diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts index 6d6e1af7b2fbcd..2d5892fa6e6cac 100644 --- a/test/functional/apps/discover/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -23,6 +23,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded( 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' ); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); await kibanaServer.uiSettings.replace({ defaultIndex: 'without-timefield', 'timepicker:timeDefaults': '{ "from": "2019-01-18T19:37:13.000Z", "to": "now"}', @@ -37,6 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload( 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' ); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); }); it('should not display a timepicker', async () => { diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.js index 6a7d414becfe7d..117b8747c5a0a8 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.js @@ -16,7 +16,8 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['settings']); - describe('filter scripted fields', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/126027 + describe.skip('filter scripted fields', function describeIndexTests() { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await browser.setWindowSize(1200, 800); diff --git a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json index 0888079ec7c52c..9998cb3a717327 100644 --- a/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json +++ b/test/functional/fixtures/es_archiver/index_pattern_without_timefield/data.json @@ -1,18 +1,3 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:without-timefield", - "index": ".kibana", - "source": { - "index-pattern": { - "fields": "[]", - "title": "without-timefield" - }, - "type": "index-pattern" - } - } -} - { "type": "doc", "value": { diff --git a/test/functional/fixtures/es_archiver/mgmt/data.json.gz b/test/functional/fixtures/es_archiver/mgmt/data.json.gz deleted file mode 100644 index c230ff8ff7e39b..00000000000000 Binary files a/test/functional/fixtures/es_archiver/mgmt/data.json.gz and /dev/null differ diff --git a/test/functional/fixtures/es_archiver/mgmt/mappings.json b/test/functional/fixtures/es_archiver/mgmt/mappings.json deleted file mode 100644 index f4962f9c476687..00000000000000 --- a/test/functional/fixtures/es_archiver/mgmt/mappings.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "dynamic": "strict", - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize_embedding/data.json.gz b/test/functional/fixtures/es_archiver/visualize_embedding/data.json.gz deleted file mode 100644 index 95b32f0ee11e5d..00000000000000 Binary files a/test/functional/fixtures/es_archiver/visualize_embedding/data.json.gz and /dev/null differ diff --git a/test/functional/fixtures/es_archiver/visualize_embedding/mappings.json b/test/functional/fixtures/es_archiver/visualize_embedding/mappings.json deleted file mode 100644 index 451369d85acd8f..00000000000000 --- a/test/functional/fixtures/es_archiver/visualize_embedding/mappings.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize_source-filters/data.json.gz b/test/functional/fixtures/es_archiver/visualize_source-filters/data.json.gz deleted file mode 100644 index c8d1c98790e597..00000000000000 Binary files a/test/functional/fixtures/es_archiver/visualize_source-filters/data.json.gz and /dev/null differ diff --git a/test/functional/fixtures/es_archiver/visualize_source-filters/mappings.json b/test/functional/fixtures/es_archiver/visualize_source-filters/mappings.json deleted file mode 100644 index 451369d85acd8f..00000000000000 --- a/test/functional/fixtures/es_archiver/visualize_source-filters/mappings.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize_source_filters/data.json.gz b/test/functional/fixtures/es_archiver/visualize_source_filters/data.json.gz deleted file mode 100644 index 238ffe3b76241e..00000000000000 Binary files a/test/functional/fixtures/es_archiver/visualize_source_filters/data.json.gz and /dev/null differ diff --git a/test/functional/fixtures/es_archiver/visualize_source_filters/mappings.json b/test/functional/fixtures/es_archiver/visualize_source_filters/mappings.json deleted file mode 100644 index ec6a9ce7f13a19..00000000000000 --- a/test/functional/fixtures/es_archiver/visualize_source_filters/mappings.json +++ /dev/null @@ -1,223 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "properties": { - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "index-pattern": { - "dynamic": "strict", - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - } - } - }, - "search": { - "dynamic": "strict", - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "dynamic": "strict", - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "url": { - "dynamic": "strict", - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "dynamic": "strict", - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/index_pattern_without_timefield.json b/test/functional/fixtures/kbn_archiver/index_pattern_without_timefield.json new file mode 100644 index 00000000000000..d5906dc8a2e990 --- /dev/null +++ b/test/functional/fixtures/kbn_archiver/index_pattern_without_timefield.json @@ -0,0 +1,30 @@ +{ + "attributes": { + "fields": "[]", + "timeFieldName": "@timestamp", + "title": "with-timefield" + }, + "coreMigrationVersion": "7.17.1", + "id": "with-timefield", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzEzLDJd" +} + +{ + "attributes": { + "fields": "[]", + "title": "without-timefield" + }, + "coreMigrationVersion": "7.17.1", + "id": "without-timefield", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "version": "WzEyLDJd" +} \ No newline at end of file diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index bc917fbf43bc4e..8fdfe77776b4ea 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -19,6 +19,7 @@ Table of Contents - [Methods](#methods) - [Executor](#executor) - [Action variables](#action-variables) + - [Recovered Alerts](#recovered-alerts) - [Licensing](#licensing) - [Documentation](#documentation) - [Tests](#tests) @@ -100,6 +101,7 @@ The following table describes the properties of the `options` object. |isExportable|Whether the rule type is exportable from the Saved Objects Management UI.|boolean| |defaultScheduleInterval|The default interval that will show up in the UI when creating a rule of this rule type.|boolean| |minimumScheduleInterval|The minimum interval that will be allowed for all rules of this rule type.|boolean| +|doesSetRecoveryContext|Whether the rule type will set context variables for recovered alerts. Defaults to `false`. If this is set to true, context variables are made available for the recovery action group and executors will be provided with the ability to set recovery context.|boolean| ### Executor @@ -170,6 +172,35 @@ This function should take the rule type params as input and extract out any save This function should take the rule type params (with saved object references) and the saved object references array as input and inject the saved object ID in place of any saved object references in the rule type params. Note that any error thrown within this function will be propagated. + +## Recovered Alerts +The Alerting framework automatically determines which alerts are recovered by comparing the active alerts from the previous rule execution to the active alerts in the current rule execution. Alerts that were active previously but not active currently are considered `recovered`. If any actions were specified on the Recovery action group for the rule, they will be scheduled at the end of the execution cycle. + +Because this determination occurs after rule type executors have completed execution, the framework provides a mechanism for rule type executors to set contextual information for recovered alerts that can be templated and used inside recovery actions. In order to use this mechanism, the rule type must set the `doesSetRecoveryContext` flag to `true` during rule type registration. + +Then, the following code would be added within a rule type executor. As you can see, when the rule type is finished creating and scheduling actions for active alerts, it should call `done()` on the alertFactory. This will give the executor access to the list recovered alerts for this execution cycle, for which it can iterate and set context. + +``` +// Create and schedule actions for active alerts +for (const i = 0; i < 5; ++i) { + alertFactory + .create('server_1') + .scheduleActions('default', { + server: 'server_1', + }); +} + +// Call done() to gain access to recovery utils +// If `doesSetRecoveryContext` is set to `false`, getRecoveredAlerts() returns an empty list +const { getRecoveredAlerts } = alertsFactory.done(); + +for (const alert of getRecoveredAlerts()) { + const alertId = alert.getId(); + alert.setContext({ + server: + }) +} +``` ## Licensing Currently most rule types are free features. But some rule types are subscription features, such as the tracking containment rule. @@ -743,6 +774,7 @@ This factory returns an instance of `Alert`. The `Alert` class has the following |scheduleActions(actionGroup, context)|Call this to schedule the execution of actions. The actionGroup is a string `id` that relates to the group of alert `actions` to execute and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert.| |scheduleActionsWithSubGroup(actionGroup, subgroup, context)|Call this to schedule the execution of actions within a subgroup. The actionGroup is a string `id` that relates to the group of alert `actions` to execute, the `subgroup` is a dynamic string that denotes a subgroup within the actionGroup and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert.| |replaceState(state)|Used to replace the current state of the alert. This doesn't work like React, the entire state must be provided. Use this feature as you see fit. The state that is set will persist between rule executions whenever you re-create an alert with the same id. The alert state will be erased when `scheduleActions` or `scheduleActionsWithSubGroup` aren't called during an execution.| +|setContext(context)|Call this to set the context for this alert that is used for templating purposes. ### When should I use `scheduleActions` and `scheduleActionsWithSubGroup`? The `scheduleActions` or `scheduleActionsWithSubGroup` methods are both used to achieve the same thing: schedule actions to be run under a specific action group. @@ -758,13 +790,16 @@ Action Subgroups are dynamic, and can be defined on the fly. This approach enables users to specify actions under specific action groups, but they can't specify actions that are specific to subgroups. As subgroups fall under action groups, we will schedule the actions specified for the action group, but the subgroup allows the RuleType implementer to reuse the same action group for multiple different active subgroups. +### When should I use `setContext`? +`setContext` is intended to be used for setting context for recovered alerts. While rule type executors make the determination as to which alerts are active for an execution, the Alerting Framework automatically determines which alerts are recovered for an execution. `setContext` empowers rule type executors to provide additional contextual information for these recovered alerts that will be templated into actions. + ## Templating Actions There needs to be a way to map rule context into action parameters. For this, we started off by adding template support. Any string within the `params` of a rule saved object's `actions` will be processed as a template and can inject context or state values. When an alert executes, the first argument is the `group` of actions to execute and the second is the context the rule exposes to templates. We iterate through each action parameter attributes recursively and render templates if they are a string. Templates have access to the following "variables": -- `context` - provided by context argument of `.scheduleActions(...)` and `.scheduleActionsWithSubGroup(...)` on an alert. +- `context` - provided by context argument of `.scheduleActions(...)`, `.scheduleActionsWithSubGroup(...)` and `setContext(...)` on an alert. - `state` - the alert's `state` provided by the most recent `replaceState` call on an alert. - `alertId` - the id of the rule - `alertInstanceId` - the alert id diff --git a/x-pack/plugins/alerting/common/rule_type.ts b/x-pack/plugins/alerting/common/rule_type.ts index 6f5f00e8f4073f..eb24e29f552b97 100644 --- a/x-pack/plugins/alerting/common/rule_type.ts +++ b/x-pack/plugins/alerting/common/rule_type.ts @@ -37,6 +37,7 @@ export interface RuleType< ruleTaskTimeout?: string; defaultScheduleInterval?: string; minimumScheduleInterval?: string; + doesSetRecoveryContext?: boolean; enabledInLicense: boolean; authorizedConsumers: Record; } diff --git a/x-pack/plugins/alerting/server/alert/alert.test.ts b/x-pack/plugins/alerting/server/alert/alert.test.ts index 83b82de9047037..eae1b18164b0f2 100644 --- a/x-pack/plugins/alerting/server/alert/alert.test.ts +++ b/x-pack/plugins/alerting/server/alert/alert.test.ts @@ -17,14 +17,21 @@ beforeAll(() => { beforeEach(() => clock.reset()); afterAll(() => clock.restore()); +describe('getId()', () => { + test('correctly sets id in constructor', () => { + const alert = new Alert('1'); + expect(alert.getId()).toEqual('1'); + }); +}); + describe('hasScheduledActions()', () => { test('defaults to false', () => { - const alert = new Alert(); + const alert = new Alert('1'); expect(alert.hasScheduledActions()).toEqual(false); }); test('returns true when scheduleActions is called', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActions('default'); expect(alert.hasScheduledActions()).toEqual(true); }); @@ -32,7 +39,7 @@ describe('hasScheduledActions()', () => { describe('isThrottled', () => { test(`should throttle when group didn't change and throttle period is still active`, () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -46,7 +53,7 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group didn't change and throttle period expired`, () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -60,7 +67,7 @@ describe('isThrottled', () => { }); test(`shouldn't throttle when group changes`, () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -76,12 +83,12 @@ describe('isThrottled', () => { describe('scheduledActionGroupOrSubgroupHasChanged()', () => { test('should be false if no last scheduled and nothing scheduled', () => { - const alert = new Alert(); + const alert = new Alert('1'); expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(false); }); test('should be false if group does not change', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -94,7 +101,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group and subgroup does not change', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -108,7 +115,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from undefined to defined', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -121,7 +128,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be false if group does not change and subgroup goes from defined to undefined', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -135,13 +142,13 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if no last scheduled and has scheduled action', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActions('default'); expect(alert.scheduledActionGroupOrSubgroupHasChanged()).toEqual(true); }); test('should be true if group does change', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -154,7 +161,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does change and subgroup does change', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -168,7 +175,7 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { }); test('should be true if group does not change and subgroup does change', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: { lastScheduledActions: { date: new Date(), @@ -184,14 +191,14 @@ describe('scheduledActionGroupOrSubgroupHasChanged()', () => { describe('getScheduledActionOptions()', () => { test('defaults to undefined', () => { - const alert = new Alert(); + const alert = new Alert('1'); expect(alert.getScheduledActionOptions()).toBeUndefined(); }); }); describe('unscheduleActions()', () => { test('makes hasScheduledActions() return false', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActions('default'); expect(alert.hasScheduledActions()).toEqual(true); alert.unscheduleActions(); @@ -199,7 +206,7 @@ describe('unscheduleActions()', () => { }); test('makes getScheduledActionOptions() return undefined', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActions('default'); expect(alert.getScheduledActionOptions()).toEqual({ actionGroup: 'default', @@ -214,7 +221,7 @@ describe('unscheduleActions()', () => { describe('getState()', () => { test('returns state passed to constructor', () => { const state = { foo: true }; - const alert = new Alert({ + const alert = new Alert('1', { state, }); expect(alert.getState()).toEqual(state); @@ -223,7 +230,7 @@ describe('getState()', () => { describe('scheduleActions()', () => { test('makes hasScheduledActions() return true', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -237,7 +244,7 @@ describe('scheduleActions()', () => { }); test('makes isThrottled() return true when throttled', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -251,7 +258,7 @@ describe('scheduleActions()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -266,7 +273,7 @@ describe('scheduleActions()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: {}, }); @@ -279,7 +286,7 @@ describe('scheduleActions()', () => { }); test('cannot schdule for execution twice', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActions('default', { field: true }); expect(() => alert.scheduleActions('default', { field: false }) @@ -291,7 +298,7 @@ describe('scheduleActions()', () => { describe('scheduleActionsWithSubGroup()', () => { test('makes hasScheduledActions() return true', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -307,7 +314,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and subgroup is the same', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -324,7 +331,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return true when throttled and last schedule had no subgroup', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -340,7 +347,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes isThrottled() return false when throttled and subgroup is the different', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -357,7 +364,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('make isThrottled() return false when throttled expired', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: { lastScheduledActions: { @@ -374,7 +381,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('makes getScheduledActionOptions() return given options', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, meta: {}, }); @@ -390,7 +397,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -400,7 +407,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice with different subgroups', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: true }); expect(() => alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -410,7 +417,7 @@ describe('scheduleActionsWithSubGroup()', () => { }); test('cannot schdule for execution twice whether there are subgroups', () => { - const alert = new Alert(); + const alert = new Alert('1'); alert.scheduleActions('default', { field: true }); expect(() => alert.scheduleActionsWithSubGroup('default', 'subgroup', { field: false }) @@ -422,7 +429,7 @@ describe('scheduleActionsWithSubGroup()', () => { describe('replaceState()', () => { test('replaces previous state', () => { - const alert = new Alert({ + const alert = new Alert('1', { state: { foo: true }, }); alert.replaceState({ bar: true }); @@ -434,7 +441,7 @@ describe('replaceState()', () => { describe('updateLastScheduledActions()', () => { test('replaces previous lastScheduledActions', () => { - const alert = new Alert({ + const alert = new Alert('1', { meta: {}, }); alert.updateLastScheduledActions('default'); @@ -450,9 +457,82 @@ describe('updateLastScheduledActions()', () => { }); }); +describe('getContext()', () => { + test('returns empty object when context has not been set', () => { + const alert = new Alert('1', { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + expect(alert.getContext()).toStrictEqual({}); + }); + + test('returns context when context has not been set', () => { + const alert = new Alert('1', { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.setContext({ field: true }); + expect(alert.getContext()).toStrictEqual({ field: true }); + }); +}); + +describe('hasContext()', () => { + test('returns true when context has been set via scheduleActions()', () => { + const alert = new Alert('1', { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.scheduleActions('default', { field: true }); + expect(alert.hasContext()).toEqual(true); + }); + + test('returns true when context has been set via setContext()', () => { + const alert = new Alert('1', { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + alert.setContext({ field: true }); + expect(alert.hasContext()).toEqual(true); + }); + + test('returns false when context has not been set', () => { + const alert = new Alert('1', { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }); + expect(alert.hasContext()).toEqual(false); + }); +}); + describe('toJSON', () => { test('only serializes state and meta', () => { const alertInstance = new Alert( + '1', { state: { foo: true }, meta: { @@ -481,6 +561,7 @@ describe('toRaw', () => { }, }; const alertInstance = new Alert( + '1', raw ); expect(alertInstance.toRaw()).toEqual(raw); diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index d34aa68ac1a11a..bf29cacf556c17 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { AlertInstanceMeta, AlertInstanceState, @@ -33,7 +34,13 @@ export type PublicAlert< ActionGroupIds extends string = DefaultActionGroupId > = Pick< Alert, - 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' + | 'getState' + | 'replaceState' + | 'scheduleActions' + | 'scheduleActionsWithSubGroup' + | 'setContext' + | 'getContext' + | 'hasContext' >; export class Alert< @@ -44,12 +51,20 @@ export class Alert< private scheduledExecutionOptions?: ScheduledExecutionOptions; private meta: AlertInstanceMeta; private state: State; + private context: Context; + private readonly id: string; - constructor({ state, meta = {} }: RawAlertInstance = {}) { + constructor(id: string, { state, meta = {} }: RawAlertInstance = {}) { + this.id = id; this.state = (state || {}) as State; + this.context = {} as Context; this.meta = meta; } + getId() { + return this.id; + } + hasScheduledActions() { return this.scheduledExecutionOptions !== undefined; } @@ -134,8 +149,17 @@ export class Alert< return this.state; } + getContext() { + return this.context; + } + + hasContext() { + return !isEmpty(this.context); + } + scheduleActions(actionGroup: ActionGroupIds, context: Context = {} as Context) { this.ensureHasNoScheduledActions(); + this.setContext(context); this.scheduledExecutionOptions = { actionGroup, context, @@ -150,6 +174,7 @@ export class Alert< context: Context = {} as Context ) { this.ensureHasNoScheduledActions(); + this.setContext(context); this.scheduledExecutionOptions = { actionGroup, subgroup, @@ -159,6 +184,11 @@ export class Alert< return this; } + setContext(context: Context) { + this.context = context; + return this; + } + private ensureHasNoScheduledActions() { if (this.hasScheduledActions()) { throw new Error('Alert instance execution has already been scheduled, cannot schedule twice'); diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts index ecb1a10bbac423..254da05c0dd53b 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.test.ts @@ -6,64 +6,206 @@ */ import sinon from 'sinon'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Alert } from './alert'; import { createAlertFactory } from './create_alert_factory'; +import { getRecoveredAlerts } from '../lib'; -let clock: sinon.SinonFakeTimers; +jest.mock('../lib', () => ({ + getRecoveredAlerts: jest.fn(), +})); -beforeAll(() => { - clock = sinon.useFakeTimers(); -}); -beforeEach(() => clock.reset()); -afterAll(() => clock.restore()); - -test('creates new alerts for ones not passed in', () => { - const alertFactory = createAlertFactory({ alerts: {} }); - const result = alertFactory.create('1'); - expect(result).toMatchInlineSnapshot(` - Object { - "meta": Object {}, - "state": Object {}, - } - `); -}); +let clock: sinon.SinonFakeTimers; +const logger = loggingSystemMock.create().get(); -test('reuses existing alerts', () => { - const alert = new Alert({ - state: { foo: true }, - meta: { lastScheduledActions: { group: 'default', date: new Date() } }, +describe('createAlertFactory()', () => { + beforeAll(() => { + clock = sinon.useFakeTimers(); }); - const alertFactory = createAlertFactory({ - alerts: { - '1': alert, - }, + beforeEach(() => clock.reset()); + afterAll(() => clock.restore()); + + test('creates new alerts for ones not passed in', () => { + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + }); + const result = alertFactory.create('1'); + expect(result).toMatchInlineSnapshot(` + Object { + "meta": Object {}, + "state": Object {}, + } + `); + expect(result.getId()).toEqual('1'); }); - const result = alertFactory.create('1'); - expect(result).toMatchInlineSnapshot(` - Object { - "meta": Object { - "lastScheduledActions": Object { - "date": "1970-01-01T00:00:00.000Z", - "group": "default", + + test('reuses existing alerts', () => { + const alert = new Alert('1', { + state: { foo: true }, + meta: { lastScheduledActions: { group: 'default', date: new Date() } }, + }); + const alertFactory = createAlertFactory({ + alerts: { + '1': alert, + }, + logger, + }); + const result = alertFactory.create('1'); + expect(result).toMatchInlineSnapshot(` + Object { + "meta": Object { + "lastScheduledActions": Object { + "date": "1970-01-01T00:00:00.000Z", + "group": "default", + }, + }, + "state": Object { + "foo": true, }, + } + `); + }); + + test('mutates given alerts', () => { + const alerts = {}; + const alertFactory = createAlertFactory({ + alerts, + logger, + }); + alertFactory.create('1'); + expect(alerts).toMatchInlineSnapshot(` + Object { + "1": Object { + "meta": Object {}, + "state": Object {}, + }, + } + `); + }); + + test('throws error when creating alerts after done() is called', () => { + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + }); + const result = alertFactory.create('1'); + expect(result).toEqual({ + meta: {}, + state: {}, + context: {}, + scheduledExecutionOptions: undefined, + id: '1', + }); + + alertFactory.done(); + + expect(() => { + alertFactory.create('2'); + }).toThrowErrorMatchingInlineSnapshot( + `"Can't create new alerts after calling done() in AlertsFactory."` + ); + }); + + test('returns recovered alerts when setsRecoveryContext is true', () => { + (getRecoveredAlerts as jest.Mock).mockReturnValueOnce({ + z: { + id: 'z', + state: { foo: true }, + meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }, - "state": Object { - "foo": true, + y: { + id: 'y', + state: { foo: true }, + meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }, - } - `); -}); + }); + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + canSetRecoveryContext: true, + }); + const result = alertFactory.create('1'); + expect(result).toEqual({ + meta: {}, + state: {}, + context: {}, + scheduledExecutionOptions: undefined, + id: '1', + }); -test('mutates given alerts', () => { - const alerts = {}; - const alertFactory = createAlertFactory({ alerts }); - alertFactory.create('1'); - expect(alerts).toMatchInlineSnapshot(` - Object { - "1": Object { - "meta": Object {}, - "state": Object {}, - }, - } - `); + const { getRecoveredAlerts: getRecoveredAlertsFn } = alertFactory.done(); + expect(getRecoveredAlertsFn).toBeDefined(); + const recoveredAlerts = getRecoveredAlertsFn!(); + expect(Array.isArray(recoveredAlerts)).toBe(true); + expect(recoveredAlerts.length).toEqual(2); + }); + + test('returns empty array if no recovered alerts', () => { + (getRecoveredAlerts as jest.Mock).mockReturnValueOnce({}); + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + canSetRecoveryContext: true, + }); + const result = alertFactory.create('1'); + expect(result).toEqual({ + meta: {}, + state: {}, + context: {}, + scheduledExecutionOptions: undefined, + id: '1', + }); + + const { getRecoveredAlerts: getRecoveredAlertsFn } = alertFactory.done(); + const recoveredAlerts = getRecoveredAlertsFn!(); + expect(Array.isArray(recoveredAlerts)).toBe(true); + expect(recoveredAlerts.length).toEqual(0); + }); + + test('returns empty array if getRecoveredAlerts returns null', () => { + (getRecoveredAlerts as jest.Mock).mockReturnValueOnce(null); + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + canSetRecoveryContext: true, + }); + const result = alertFactory.create('1'); + expect(result).toEqual({ + meta: {}, + state: {}, + context: {}, + scheduledExecutionOptions: undefined, + id: '1', + }); + + const { getRecoveredAlerts: getRecoveredAlertsFn } = alertFactory.done(); + const recoveredAlerts = getRecoveredAlertsFn!(); + expect(Array.isArray(recoveredAlerts)).toBe(true); + expect(recoveredAlerts.length).toEqual(0); + }); + + test('returns empty array if recovered alerts exist but setsRecoveryContext is false', () => { + const alertFactory = createAlertFactory({ + alerts: {}, + logger, + canSetRecoveryContext: false, + }); + const result = alertFactory.create('1'); + expect(result).toEqual({ + meta: {}, + state: {}, + context: {}, + scheduledExecutionOptions: undefined, + id: '1', + }); + + const { getRecoveredAlerts: getRecoveredAlertsFn } = alertFactory.done(); + const recoveredAlerts = getRecoveredAlertsFn!(); + expect(Array.isArray(recoveredAlerts)).toBe(true); + expect(recoveredAlerts.length).toEqual(0); + expect(logger.debug).toHaveBeenCalledWith( + `Set doesSetRecoveryContext to true on rule type to get access to recovered alerts.` + ); + }); }); diff --git a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts index 07f4dbc7b20ea6..ad83b0a416c727 100644 --- a/x-pack/plugins/alerting/server/alert/create_alert_factory.ts +++ b/x-pack/plugins/alerting/server/alert/create_alert_factory.ts @@ -5,8 +5,18 @@ * 2.0. */ +import { Logger } from 'src/core/server'; import { AlertInstanceContext, AlertInstanceState } from '../types'; import { Alert } from './alert'; +import { getRecoveredAlerts } from '../lib'; + +export interface AlertFactoryDoneUtils< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + ActionGroupIds extends string +> { + getRecoveredAlerts: () => Array>; +} export interface CreateAlertFactoryOpts< InstanceState extends AlertInstanceState, @@ -14,20 +24,50 @@ export interface CreateAlertFactoryOpts< ActionGroupIds extends string > { alerts: Record>; + logger: Logger; + canSetRecoveryContext?: boolean; } export function createAlertFactory< InstanceState extends AlertInstanceState, InstanceContext extends AlertInstanceContext, ActionGroupIds extends string ->({ alerts }: CreateAlertFactoryOpts) { +>({ + alerts, + logger, + canSetRecoveryContext = false, +}: CreateAlertFactoryOpts) { + // Keep track of which alerts we started with so we can determine which have recovered + const initialAlertIds = new Set(Object.keys(alerts)); + let isDone = false; return { create: (id: string): Alert => { + if (isDone) { + throw new Error(`Can't create new alerts after calling done() in AlertsFactory.`); + } if (!alerts[id]) { - alerts[id] = new Alert(); + alerts[id] = new Alert(id); } return alerts[id]; }, + done: (): AlertFactoryDoneUtils => { + isDone = true; + return { + getRecoveredAlerts: () => { + if (!canSetRecoveryContext) { + logger.debug( + `Set doesSetRecoveryContext to true on rule type to get access to recovered alerts.` + ); + return []; + } + + const recoveredAlerts = getRecoveredAlerts(alerts, initialAlertIds); + return Object.keys(recoveredAlerts ?? []).map( + (alertId: string) => recoveredAlerts[alertId] + ); + }, + }; + }, }; } diff --git a/x-pack/plugins/alerting/server/alert/index.ts b/x-pack/plugins/alerting/server/alert/index.ts index 5e1a9ee626b571..2b5dc4791037e7 100644 --- a/x-pack/plugins/alerting/server/alert/index.ts +++ b/x-pack/plugins/alerting/server/alert/index.ts @@ -8,3 +8,4 @@ export type { PublicAlert } from './alert'; export { Alert } from './alert'; export { createAlertFactory } from './create_alert_factory'; +export type { AlertFactoryDoneUtils } from './create_alert_factory'; diff --git a/x-pack/plugins/alerting/server/lib/get_recovered_alerts.test.ts b/x-pack/plugins/alerting/server/lib/get_recovered_alerts.test.ts new file mode 100644 index 00000000000000..b984b04fc65d49 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_recovered_alerts.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { getRecoveredAlerts } from './get_recovered_alerts'; +import { Alert } from '../alert'; +import { AlertInstanceState, AlertInstanceContext, DefaultActionGroupId } from '../types'; + +describe('getRecoveredAlerts', () => { + test('considers alert recovered if it has no scheduled actions', () => { + const alert1 = new Alert('1'); + alert1.scheduleActions('default', { foo: '1' }); + + const alert2 = new Alert('2'); + alert2.setContext({ foo: '2' }); + const alerts = { + '1': alert1, + '2': alert2, + }; + + expect(getRecoveredAlerts(alerts, new Set(['1', '2']))).toEqual({ + '2': alert2, + }); + }); + + test('does not consider alert recovered if it has no actions but was not in original alerts list', () => { + const alert1 = new Alert('1'); + alert1.scheduleActions('default', { foo: '1' }); + const alert2 = new Alert('2'); + const alerts = { + '1': alert1, + '2': alert2, + }; + + expect(getRecoveredAlerts(alerts, new Set(['1']))).toEqual({}); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_recovered_alerts.ts b/x-pack/plugins/alerting/server/lib/get_recovered_alerts.ts new file mode 100644 index 00000000000000..f389f56a813d04 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_recovered_alerts.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Dictionary, pickBy } from 'lodash'; +import { Alert } from '../alert'; +import { AlertInstanceState, AlertInstanceContext } from '../types'; + +export function getRecoveredAlerts< + InstanceState extends AlertInstanceState, + InstanceContext extends AlertInstanceContext, + RecoveryActionGroupId extends string +>( + alerts: Record>, + originalAlertIds: Set +): Dictionary> { + return pickBy( + alerts, + (alert: Alert, id) => + !alert.hasScheduledActions() && originalAlertIds.has(id) + ); +} diff --git a/x-pack/plugins/alerting/server/lib/index.ts b/x-pack/plugins/alerting/server/lib/index.ts index 29526f17268f25..a5fa1b29c3044a 100644 --- a/x-pack/plugins/alerting/server/lib/index.ts +++ b/x-pack/plugins/alerting/server/lib/index.ts @@ -24,3 +24,4 @@ export { ruleExecutionStatusToRaw, ruleExecutionStatusFromRaw, } from './rule_execution_status'; +export { getRecoveredAlerts } from './get_recovered_alerts'; diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index afbc3ef9cec43a..f7872ba7978564 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -7,7 +7,7 @@ import { rulesClientMock } from './rules_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { Alert } from './alert'; +import { Alert, AlertFactoryDoneUtils } from './alert'; import { elasticsearchServiceMock, savedObjectsClientMock, @@ -64,6 +64,17 @@ const createAlertFactoryMock = { return mock as unknown as AlertInstanceMock; }, + done: < + InstanceState extends AlertInstanceState = AlertInstanceState, + InstanceContext extends AlertInstanceContext = AlertInstanceContext, + ActionGroupIds extends string = string + >() => { + const mock: jest.Mocked> = + { + getRecoveredAlerts: jest.fn().mockReturnValue([]), + }; + return mock; + }, }; const createAbortableSearchClientMock = () => { @@ -86,9 +97,11 @@ const createAlertServicesMock = < InstanceContext extends AlertInstanceContext = AlertInstanceContext >() => { const alertFactoryMockCreate = createAlertFactoryMock.create(); + const alertFactoryMockDone = createAlertFactoryMock.done(); return { alertFactory: { create: jest.fn().mockReturnValue(alertFactoryMockCreate), + done: jest.fn().mockReturnValue(alertFactoryMockDone), }, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 70aad0d6921e16..ac3253346138a2 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -293,6 +293,7 @@ export class AlertingPlugin { ruleType.ruleTaskTimeout = ruleType.ruleTaskTimeout ?? config.defaultRuleTaskTimeout; ruleType.cancelAlertsOnRuleTimeout = ruleType.cancelAlertsOnRuleTimeout ?? config.cancelAlertsOnRuleTimeout; + ruleType.doesSetRecoveryContext = ruleType.doesSetRecoveryContext ?? false; ruleTypeRegistry.register(ruleType); }); }, diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index 7deb2704fb7ecc..752f729fb8e38f 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -60,6 +60,7 @@ describe('ruleTypesRoute', () => { enabledInLicense: true, minimumScheduleInterval: '1m', defaultScheduleInterval: '10m', + doesSetRecoveryContext: false, } as RegistryAlertTypeWithAuth, ]; const expectedResult: Array> = [ @@ -74,6 +75,7 @@ describe('ruleTypesRoute', () => { ], default_action_group_id: 'default', default_schedule_interval: '10m', + does_set_recovery_context: false, minimum_license_required: 'basic', minimum_schedule_interval: '1m', is_exportable: true, @@ -109,6 +111,7 @@ describe('ruleTypesRoute', () => { "authorized_consumers": Object {}, "default_action_group_id": "default", "default_schedule_interval": "10m", + "does_set_recovery_context": false, "enabled_in_license": true, "id": "1", "is_exportable": true, diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts index d1f24538d76d8d..7b2a0c63be1985 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -25,6 +25,7 @@ const rewriteBodyRes: RewriteResponseCase = (result authorizedConsumers, minimumScheduleInterval, defaultScheduleInterval, + doesSetRecoveryContext, ...rest }) => ({ ...rest, @@ -39,6 +40,7 @@ const rewriteBodyRes: RewriteResponseCase = (result authorized_consumers: authorizedConsumers, minimum_schedule_interval: minimumScheduleInterval, default_schedule_interval: defaultScheduleInterval, + does_set_recovery_context: doesSetRecoveryContext, }) ); }; diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index e23c7f25a4f76b..8ba2847486bca2 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -493,6 +493,7 @@ describe('list()', () => { }, ], defaultActionGroupId: 'testActionGroup', + doesSetRecoveryContext: false, isExportable: true, ruleTaskTimeout: '20m', minimumLicenseRequired: 'basic', @@ -520,6 +521,7 @@ describe('list()', () => { }, "defaultActionGroupId": "testActionGroup", "defaultScheduleInterval": undefined, + "doesSetRecoveryContext": false, "enabledInLicense": false, "id": "test", "isExportable": true, diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index 9b4f94f3510bef..6673fb630ef59b 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -51,6 +51,7 @@ export interface RegistryRuleType | 'ruleTaskTimeout' | 'minimumScheduleInterval' | 'defaultScheduleInterval' + | 'doesSetRecoveryContext' > { id: string; enabledInLicense: boolean; @@ -331,6 +332,7 @@ export class RuleTypeRegistry { ruleTaskTimeout, minimumScheduleInterval, defaultScheduleInterval, + doesSetRecoveryContext, }, ]: [string, UntypedNormalizedRuleType]) => ({ id, @@ -345,6 +347,7 @@ export class RuleTypeRegistry { ruleTaskTimeout, minimumScheduleInterval, defaultScheduleInterval, + doesSetRecoveryContext, enabledInLicense: !!this.licenseState.getLicenseCheckForRuleType( id, name, diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 63e35583bc9a1d..72ef2dba89ce7d 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1337,7 +1337,7 @@ export class RulesClient { const recoveredAlertInstances = mapValues, Alert>( state.alertInstances ?? {}, - (rawAlertInstance) => new Alert(rawAlertInstance) + (rawAlertInstance, alertId) => new Alert(alertId, rawAlertInstance) ); const recoveredAlertInstanceIds = Object.keys(recoveredAlertInstances); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 51d50c398c6f5a..8bc4ad280873ef 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -66,6 +66,7 @@ import { Event, } from '../lib/create_alert_event_log_record_object'; import { createAbortableEsClientFactory } from '../lib/create_abortable_es_client_factory'; +import { getRecoveredAlerts } from '../lib'; const FALLBACK_RETRY_INTERVAL = '5m'; const CONNECTIVITY_RETRY_INTERVAL = '5m'; @@ -334,7 +335,11 @@ export class TaskRunner< const alerts = mapValues< Record, CreatedAlert - >(alertRawInstances, (rawAlert) => new CreatedAlert(rawAlert)); + >( + alertRawInstances, + (rawAlert, alertId) => new CreatedAlert(alertId, rawAlert) + ); + const originalAlerts = cloneDeep(alerts); const originalAlertIds = new Set(Object.keys(originalAlerts)); @@ -364,6 +369,8 @@ export class TaskRunner< WithoutReservedActionGroups >({ alerts, + logger: this.logger, + canSetRecoveryContext: ruleType.doesSetRecoveryContext ?? false, }), shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(), shouldStopExecution: () => this.cancelled, @@ -424,17 +431,15 @@ export class TaskRunner< alerts, (alert: CreatedAlert) => alert.hasScheduledActions() ); - const recoveredAlerts = pickBy( - alerts, - (alert: CreatedAlert, id) => - !alert.hasScheduledActions() && originalAlertIds.has(id) - ); + + const recoveredAlerts = getRecoveredAlerts(alerts, originalAlertIds); logActiveAndRecoveredAlerts({ logger: this.logger, activeAlerts: alertsWithScheduledActions, recoveredAlerts, ruleLabel, + canSetRecoveryContext: ruleType.doesSetRecoveryContext ?? false, }); trackAlertDurations({ @@ -1155,7 +1160,7 @@ async function scheduleActionsForRecoveredAlerts< alert.unscheduleActions(); const triggeredActionsForRecoveredAlert = await executionHandler({ actionGroup: recoveryActionGroup.id, - context: {}, + context: alert.getContext(), state: {}, alertId: id, }); @@ -1176,6 +1181,7 @@ interface LogActiveAndRecoveredAlertsParams< activeAlerts: Dictionary>; recoveredAlerts: Dictionary>; ruleLabel: string; + canSetRecoveryContext: boolean; } function logActiveAndRecoveredAlerts< @@ -1191,7 +1197,7 @@ function logActiveAndRecoveredAlerts< RecoveryActionGroupId > ) { - const { logger, activeAlerts, recoveredAlerts, ruleLabel } = params; + const { logger, activeAlerts, recoveredAlerts, ruleLabel, canSetRecoveryContext } = params; const activeAlertIds = Object.keys(activeAlerts); const recoveredAlertIds = Object.keys(recoveredAlerts); @@ -1218,6 +1224,16 @@ function logActiveAndRecoveredAlerts< recoveredAlertIds )}` ); + + if (canSetRecoveryContext) { + for (const id of recoveredAlertIds) { + if (!recoveredAlerts[id].hasContext()) { + logger.debug( + `rule ${ruleLabel} has no recovery context specified for recovered alert ${id}` + ); + } + } + } } } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 9d6302774f889f..50acb67a3de479 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,7 +7,7 @@ import type { IRouter, RequestHandlerContext, SavedObjectReference } from 'src/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PublicAlert } from './alert'; +import { AlertFactoryDoneUtils, PublicAlert } from './alert'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -76,6 +76,7 @@ export interface AlertServices< > extends Services { alertFactory: { create: (id: string) => PublicAlert; + done: () => AlertFactoryDoneUtils; }; shouldWriteAlerts: () => boolean; shouldStopExecution: () => boolean; @@ -167,6 +168,7 @@ export interface RuleType< minimumScheduleInterval?: string; ruleTaskTimeout?: string; cancelAlertsOnRuleTimeout?: boolean; + doesSetRecoveryContext?: boolean; } export type UntypedRuleType = RuleType< AlertTypeParams, diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index f881b4476fe224..a34b3cdb1334df 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -42,7 +42,7 @@ export const createRuleTypeMocks = () => { savedObjectsClient: { get: () => ({ attributes: { consumer: APM_SERVER_FEATURE_ID } }), }, - alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, + alertFactory: { create: jest.fn(() => ({ scheduleActions })), done: {} }, alertWithLifecycle: jest.fn(), logger: loggerMock, shouldWriteAlerts: () => true, diff --git a/x-pack/plugins/fleet/.storybook/context/stubs.tsx b/x-pack/plugins/fleet/.storybook/context/stubs.tsx index f72b176bd8d7be..65485a31d376ac 100644 --- a/x-pack/plugins/fleet/.storybook/context/stubs.tsx +++ b/x-pack/plugins/fleet/.storybook/context/stubs.tsx @@ -8,8 +8,10 @@ import type { FleetStartServices } from '../../public/plugin'; type Stubs = + | 'licensing' | 'storage' | 'data' + | 'fieldFormats' | 'deprecations' | 'fatalErrors' | 'navigation' @@ -19,8 +21,10 @@ type Stubs = type StubbedStartServices = Pick; export const stubbedStartServices: StubbedStartServices = { + licensing: {} as FleetStartServices['licensing'], storage: {} as FleetStartServices['storage'], data: {} as FleetStartServices['data'], + fieldFormats: {} as FleetStartServices['fieldFormats'], deprecations: {} as FleetStartServices['deprecations'], fatalErrors: {} as FleetStartServices['fatalErrors'], navigation: {} as FleetStartServices['navigation'], diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index 29a491fe0c932b..b5872b0a995a9b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -216,7 +216,6 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { * and no routes defined */ export const FleetAppContext: React.FC<{ - basepath: string; startServices: FleetStartServices; config: FleetConfigType; history: AppMountParameters['history']; diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index 9c6319a92b2ee0..8946e6af0ce759 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -31,7 +31,6 @@ export const ProtectedRoute: React.FunctionComponent = ({ }; interface FleetAppProps { - basepath: string; startServices: FleetStartServices; config: FleetConfigType; history: AppMountParameters['history']; @@ -41,7 +40,6 @@ interface FleetAppProps { theme$: AppMountParameters['theme$']; } const FleetApp = ({ - basepath, startServices, config, history, @@ -52,7 +50,6 @@ const FleetApp = ({ }: FleetAppProps) => { return ( void; + updateAgentPolicy: (u: AgentPolicy | null, errorMessage?: JSX.Element) => void; isFleetServerPolicy?: boolean; agentPolicyName: string; } @@ -84,12 +84,24 @@ export const AgentPolicyCreateInlineForm: React.FunctionComponent = ({ updateAgentPolicy(resp.data.item); } } catch (e) { - updateAgentPolicy(null); + updateAgentPolicy(null, mapError(e)); } finally { setIsLoading(false); } }, [newAgentPolicy, withSysMonitoring, updateAgentPolicy]); + function mapError(e: { statusCode: number }): JSX.Element | undefined { + switch (e.statusCode) { + case 409: + return ( + + ); + } + } + return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_created_callout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_created_callout.tsx index ba3af7716d985c..b73bd7251f8533 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_created_callout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_policy_created_callout.tsx @@ -8,21 +8,27 @@ import React from 'react'; import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; + export enum CREATE_STATUS { INITIAL = 'initial', CREATED = 'created', FAILED = 'failed', } +export interface AgentPolicyCreateState { + status: CREATE_STATUS; + errorMessage?: JSX.Element; +} + interface Props { - createStatus: CREATE_STATUS; + createState: AgentPolicyCreateState; } -export const AgentPolicyCreatedCallOut: React.FunctionComponent = ({ createStatus }) => { +export const AgentPolicyCreatedCallOut: React.FunctionComponent = ({ createState }) => { return ( <> - {createStatus === CREATE_STATUS.CREATED ? ( + {createState.status === CREATE_STATUS.CREATED ? ( = ({ crea } color="danger" iconType="cross" - /> + > + {createState.errorMessage ?? null} + )} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx index aed6c347120122..29ea4102bc1efb 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/data_stream/list_page/index.tsx @@ -27,9 +27,7 @@ import { DataStreamRowActions } from './components/data_stream_row_actions'; export const DataStreamListPage: React.FunctionComponent<{}> = () => { useBreadcrumbs('data_streams'); - const { - data: { fieldFormats }, - } = useStartServices(); + const { fieldFormats } = useStartServices(); const { pagination, pageSizeOptions } = usePagination(); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx index 87382ac70a9bb6..a8354237bbcb7c 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_select_create.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useEffect } from 'react'; +import type { AgentPolicyCreateState } from '../../applications/fleet/sections/agents/components'; import { AgentPolicyCreatedCallOut, CREATE_STATUS, @@ -40,7 +41,9 @@ export const SelectCreateAgentPolicy: React.FC = ({ }) => { const [showCreatePolicy, setShowCreatePolicy] = useState(agentPolicies.length === 0); - const [createStatus, setCreateStatus] = useState(CREATE_STATUS.INITIAL); + const [createState, setCreateState] = useState({ + status: CREATE_STATUS.INITIAL, + }); const [newName, setNewName] = useState(incrementPolicyName(agentPolicies, isFleetServerPolicy)); @@ -52,13 +55,13 @@ export const SelectCreateAgentPolicy: React.FC = ({ }, [agentPolicies, isFleetServerPolicy]); const onAgentPolicyCreated = useCallback( - async (policy: AgentPolicy | null) => { + async (policy: AgentPolicy | null, errorMessage?: JSX.Element) => { if (!policy) { - setCreateStatus(CREATE_STATUS.FAILED); + setCreateState({ status: CREATE_STATUS.FAILED, errorMessage }); return; } setShowCreatePolicy(false); - setCreateStatus(CREATE_STATUS.CREATED); + setCreateState({ status: CREATE_STATUS.CREATED }); if (onAgentPolicyChange) { onAgentPolicyChange(policy.id, policy!); } @@ -88,8 +91,8 @@ export const SelectCreateAgentPolicy: React.FC = ({ isFleetServerPolicy={isFleetServerPolicy} /> )} - {createStatus !== CREATE_STATUS.INITIAL && ( - + {createState.status !== CREATE_STATUS.INITIAL && ( + )} ); diff --git a/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx index 22b58c14fb0722..5952fbebcf272f 100644 --- a/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx +++ b/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx @@ -77,7 +77,6 @@ export const createFleetTestRendererMock = (): TestRenderer => { AppWrapper: memo(({ children }) => { return ( { const cloud = cloudMock.createSetup(); return { - licensing: licensingMock.createSetup(), data: dataPluginMock.createSetupContract(), home: homePluginMock.createSetupContract(), customIntegrations: customIntegrationsMock.createSetup(), @@ -26,7 +26,9 @@ export const createSetupDepsMock = () => { export const createStartDepsMock = () => { return { + licensing: licensingMock.createStart(), data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract() as any, navigation: navigationPluginMock.createStartContract(), customIntegrations: customIntegrationsMock.createStart(), share: sharePluginMock.createStartContract(), diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 385ef2bee65124..79dc8cc38c4bf5 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -36,10 +36,11 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; +import type { FieldFormatsStart } from '../../../../src/plugins/field_formats/public/index'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import type { LicensingPluginSetup } from '../../licensing/public'; +import type { LicensingPluginStart } from '../../licensing/public'; import type { CloudSetup } from '../../cloud/public'; import type { GlobalSearchPluginSetup } from '../../global_search/public'; import { @@ -82,7 +83,6 @@ export interface FleetStart { } export interface FleetSetupDeps { - licensing: LicensingPluginSetup; data: DataPublicPluginSetup; home?: HomePublicPluginSetup; cloud?: CloudSetup; @@ -92,7 +92,9 @@ export interface FleetSetupDeps { } export interface FleetStartDeps { + licensing: LicensingPluginStart; data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; navigation: NavigationPublicPluginStart; customIntegrations: CustomIntegrationsStart; share: SharePluginStart; @@ -129,9 +131,6 @@ export class FleetPlugin implements Plugin(appRoutesService.getCheckPermissionsPath()) ); + // Set up license service + licenseService.start(deps.licensing.license$); + registerExtension({ package: CUSTOM_LOGS_INTEGRATION_NAME, view: 'package-detail-assets', diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 4bc06d51e7e0b6..272e92fca6eae8 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -14,7 +14,7 @@ import type { CoreStart, ElasticsearchServiceStart, Logger, - AsyncPlugin, + Plugin, PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, @@ -33,7 +33,7 @@ import { ServiceStatusLevels, } from '../../../../src/core/server'; import type { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import type { LicensingPluginSetup, ILicense } from '../../licensing/server'; +import type { LicensingPluginStart } from '../../licensing/server'; import type { EncryptedSavedObjectsPluginStart, EncryptedSavedObjectsPluginSetup, @@ -93,7 +93,6 @@ import { TelemetryEventsSender } from './telemetry/sender'; import { setupFleet } from './services/setup'; export interface FleetSetupDeps { - licensing: LicensingPluginSetup; security: SecurityPluginSetup; features?: FeaturesPluginSetup; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; @@ -105,6 +104,7 @@ export interface FleetSetupDeps { export interface FleetStartDeps { data: DataPluginStart; + licensing: LicensingPluginStart; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security: SecurityPluginStart; telemetry?: TelemetryPluginStart; @@ -175,9 +175,8 @@ export interface FleetStartContract { } export class FleetPlugin - implements AsyncPlugin + implements Plugin { - private licensing$!: Observable; private config$: Observable; private configInitialValue: FleetConfigType; private cloud?: CloudSetup; @@ -212,7 +211,6 @@ export class FleetPlugin public setup(core: CoreSetup, deps: FleetSetupDeps) { this.httpSetup = core.http; - this.licensing$ = deps.licensing.license$; this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects; this.cloud = deps.cloud; this.securitySetup = deps.security; @@ -384,7 +382,6 @@ export class FleetPlugin this.telemetryEventsSender.setup(deps.telemetry); } - public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract { appContextService.start({ elasticsearch: core.elasticsearch, @@ -404,7 +401,7 @@ export class FleetPlugin logger: this.logger, telemetryEventsSender: this.telemetryEventsSender, }); - licenseService.start(this.licensing$); + licenseService.start(plugins.licensing.license$); this.telemetryEventsSender.start(plugins.telemetry, core); diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts index af3b3ef4dfccda..402b65334121af 100644 --- a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts @@ -7,11 +7,9 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; -import type { Installation } from '../../common'; - -import { shouldUpgradePolicies, upgradeManagedPackagePolicies } from './managed_package_policies'; +import { upgradeManagedPackagePolicies } from './managed_package_policies'; import { packagePolicyService } from './package_policy'; -import { getInstallation } from './epm/packages'; +import { getInstallations } from './epm/packages'; jest.mock('./package_policy'); jest.mock('./epm/packages'); @@ -20,7 +18,7 @@ jest.mock('./app_context', () => { ...jest.requireActual('./app_context'), appContextService: { getLogger: jest.fn(() => { - return { error: jest.fn() }; + return { error: jest.fn(), debug: jest.fn() }; }), }, }; @@ -28,20 +26,30 @@ jest.mock('./app_context', () => { describe('upgradeManagedPackagePolicies', () => { afterEach(() => { - (packagePolicyService.get as jest.Mock).mockReset(); - (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockReset(); - (getInstallation as jest.Mock).mockReset(); - (packagePolicyService.upgrade as jest.Mock).mockReset(); + jest.clearAllMocks(); }); it('should not upgrade policies for non-managed package', async () => { const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const soClient = savedObjectsClientMock.create(); - (packagePolicyService.get as jest.Mock).mockImplementationOnce( - (savedObjectsClient: any, id: string) => { - return { - id, + (getInstallations as jest.Mock).mockResolvedValueOnce({ + saved_objects: [], + }); + + await upgradeManagedPackagePolicies(soClient, esClient); + + expect(packagePolicyService.upgrade).not.toBeCalled(); + }); + + it('should upgrade policies for managed package', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const soClient = savedObjectsClientMock.create(); + + (packagePolicyService.list as jest.Mock).mockResolvedValueOnce({ + items: [ + { + id: 'managed-package-id', inputs: {}, version: '', revision: 1, @@ -50,43 +58,48 @@ describe('upgradeManagedPackagePolicies', () => { created_at: '', created_by: '', package: { - name: 'non-managed-package', - title: 'Non-Managed Package', - version: '1.0.0', + name: 'managed-package', + title: 'Managed Package', + version: '0.0.1', }, - }; - } - ); + }, + ], + }); - (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockImplementationOnce( - (savedObjectsClient: any, id: string) => { - return { - name: 'non-managed-package-policy', - diff: [{ id: 'foo' }, { id: 'bar' }], - hasErrors: false, - }; - } - ); + (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockResolvedValueOnce({ + name: 'non-managed-package-policy', + diff: [{ id: 'foo' }, { id: 'bar' }], + hasErrors: false, + }); - (getInstallation as jest.Mock).mockResolvedValueOnce({ - id: 'test-installation', - version: '0.0.1', - keep_policies_up_to_date: false, + (getInstallations as jest.Mock).mockResolvedValueOnce({ + saved_objects: [ + { + attributes: { + id: 'test-installation', + version: '1.0.0', + keep_policies_up_to_date: true, + }, + }, + ], }); - await upgradeManagedPackagePolicies(soClient, esClient, ['non-managed-package-id']); + const results = await upgradeManagedPackagePolicies(soClient, esClient); + expect(results).toEqual([ + { packagePolicyId: 'managed-package-id', diff: [{ id: 'foo' }, { id: 'bar' }], errors: [] }, + ]); - expect(packagePolicyService.upgrade).not.toBeCalled(); + expect(packagePolicyService.upgrade).toBeCalledWith(soClient, esClient, ['managed-package-id']); }); - it('should upgrade policies for managed package', async () => { + it('should not upgrade policy if newer than installed package version', async () => { const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const soClient = savedObjectsClientMock.create(); - (packagePolicyService.get as jest.Mock).mockImplementationOnce( - (savedObjectsClient: any, id: string) => { - return { - id, + (packagePolicyService.list as jest.Mock).mockResolvedValueOnce({ + items: [ + { + id: 'managed-package-id', inputs: {}, version: '', revision: 1, @@ -97,31 +110,28 @@ describe('upgradeManagedPackagePolicies', () => { package: { name: 'managed-package', title: 'Managed Package', - version: '0.0.1', + version: '1.0.1', }, - }; - } - ); - - (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockImplementationOnce( - (savedObjectsClient: any, id: string) => { - return { - name: 'non-managed-package-policy', - diff: [{ id: 'foo' }, { id: 'bar' }], - hasErrors: false, - }; - } - ); + }, + ], + }); - (getInstallation as jest.Mock).mockResolvedValueOnce({ - id: 'test-installation', - version: '1.0.0', - keep_policies_up_to_date: true, + (getInstallations as jest.Mock).mockResolvedValueOnce({ + saved_objects: [ + { + attributes: { + id: 'test-installation', + version: '1.0.0', + keep_policies_up_to_date: true, + }, + }, + ], }); - await upgradeManagedPackagePolicies(soClient, esClient, ['managed-package-id']); + await upgradeManagedPackagePolicies(soClient, esClient); - expect(packagePolicyService.upgrade).toBeCalledWith(soClient, esClient, ['managed-package-id']); + expect(packagePolicyService.getUpgradeDryRunDiff).not.toHaveBeenCalled(); + expect(packagePolicyService.upgrade).not.toHaveBeenCalled(); }); describe('when dry run reports conflicts', () => { @@ -129,10 +139,10 @@ describe('upgradeManagedPackagePolicies', () => { const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const soClient = savedObjectsClientMock.create(); - (packagePolicyService.get as jest.Mock).mockImplementationOnce( - (savedObjectsClient: any, id: string) => { - return { - id, + (packagePolicyService.list as jest.Mock).mockResolvedValueOnce({ + items: [ + { + id: 'conflicting-package-policy', inputs: {}, version: '', revision: 1, @@ -145,32 +155,32 @@ describe('upgradeManagedPackagePolicies', () => { title: 'Conflicting Package', version: '0.0.1', }, - }; - } - ); + }, + ], + }); - (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockImplementationOnce( - (savedObjectsClient: any, id: string) => { - return { - name: 'conflicting-package-policy', - diff: [ - { id: 'foo' }, - { id: 'bar', errors: [{ key: 'some.test.value', message: 'Conflict detected' }] }, - ], - hasErrors: true, - }; - } - ); + (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockResolvedValueOnce({ + name: 'conflicting-package-policy', + diff: [ + { id: 'foo' }, + { id: 'bar', errors: [{ key: 'some.test.value', message: 'Conflict detected' }] }, + ], + hasErrors: true, + }); - (getInstallation as jest.Mock).mockResolvedValueOnce({ - id: 'test-installation', - version: '1.0.0', - keep_policies_up_to_date: true, + (getInstallations as jest.Mock).mockResolvedValueOnce({ + saved_objects: [ + { + attributes: { + id: 'test-installation', + version: '1.0.0', + keep_policies_up_to_date: true, + }, + }, + ], }); - const result = await upgradeManagedPackagePolicies(soClient, esClient, [ - 'conflicting-package-policy', - ]); + const result = await upgradeManagedPackagePolicies(soClient, esClient); expect(result).toEqual([ { @@ -202,61 +212,3 @@ describe('upgradeManagedPackagePolicies', () => { }); }); }); - -describe('shouldUpgradePolicies', () => { - describe('package policy is up-to-date', () => { - describe('keep_policies_up_to_date is true', () => { - it('returns false', () => { - const installedPackage = { - version: '1.0.0', - keep_policies_up_to_date: true, - }; - - const result = shouldUpgradePolicies('1.0.0', installedPackage as Installation); - - expect(result).toBe(false); - }); - }); - - describe('keep_policies_up_to_date is false', () => { - it('returns false', () => { - const installedPackage = { - version: '1.0.0', - keep_policies_up_to_date: false, - }; - - const result = shouldUpgradePolicies('1.0.0', installedPackage as Installation); - - expect(result).toBe(false); - }); - }); - }); - - describe('package policy is out-of-date', () => { - describe('keep_policies_up_to_date is true', () => { - it('returns true', () => { - const installedPackage = { - version: '1.1.0', - keep_policies_up_to_date: true, - }; - - const result = shouldUpgradePolicies('1.0.0', installedPackage as Installation); - - expect(result).toBe(true); - }); - }); - - describe('keep_policies_up_to_date is false', () => { - it('returns false', () => { - const installedPackage = { - version: '1.1.0', - keep_policies_up_to_date: false, - }; - - const result = shouldUpgradePolicies('1.0.0', installedPackage as Installation); - - expect(result).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.ts index 77715ad488feb6..2c4b326d565325 100644 --- a/x-pack/plugins/fleet/server/services/managed_package_policies.ts +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.ts @@ -6,12 +6,16 @@ */ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; -import semverGte from 'semver/functions/gte'; +import semverLt from 'semver/functions/lt'; -import type { Installation, UpgradePackagePolicyDryRunResponseItem } from '../../common'; +import type { UpgradePackagePolicyDryRunResponseItem } from '../../common'; + +import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants'; + +import type { Installation, PackagePolicy } from '../types'; import { appContextService } from './app_context'; -import { getInstallation } from './epm/packages'; +import { getInstallations } from './epm/packages'; import { packagePolicyService } from './package_policy'; export interface UpgradeManagedPackagePoliciesResult { @@ -26,78 +30,87 @@ export interface UpgradeManagedPackagePoliciesResult { */ export const upgradeManagedPackagePolicies = async ( soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - packagePolicyIds: string[] + esClient: ElasticsearchClient ): Promise => { + appContextService + .getLogger() + .debug('Running required package policies upgrades for managed policies'); const results: UpgradeManagedPackagePoliciesResult[] = []; - for (const packagePolicyId of packagePolicyIds) { - const packagePolicy = await packagePolicyService.get(soClient, packagePolicyId); - - if (!packagePolicy || !packagePolicy.package) { - continue; - } - - const installedPackage = await getInstallation({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - }); + const installedPackages = await getInstallations(soClient, { + filter: `${PACKAGES_SAVED_OBJECT_TYPE}.attributes.install_status:installed AND ${PACKAGES_SAVED_OBJECT_TYPE}.attributes.keep_policies_up_to_date:true`, + }); - if (!installedPackage) { - results.push({ - packagePolicyId, - errors: [`${packagePolicy.package.name} is not installed`], - }); - - continue; - } - - if (shouldUpgradePolicies(packagePolicy.package.version, installedPackage)) { - // Since upgrades don't report diffs/errors, we need to perform a dry run first in order - // to notify the user of any granular policy upgrade errors that occur during Fleet's - // preconfiguration check - const dryRunResults = await packagePolicyService.getUpgradeDryRunDiff( - soClient, - packagePolicyId - ); - - if (dryRunResults.hasErrors) { - const errors = dryRunResults.diff - ? dryRunResults.diff?.[1].errors - : [dryRunResults.body?.message]; - - appContextService - .getLogger() - .error( - new Error( - `Error upgrading package policy ${packagePolicyId}: ${JSON.stringify(errors)}` - ) - ); - - results.push({ packagePolicyId, diff: dryRunResults.diff, errors }); - continue; - } + for (const { attributes: installedPackage } of installedPackages.saved_objects) { + const packagePolicies = await getPackagePoliciesNotMatchingVersion( + soClient, + installedPackage.name, + installedPackage.version + ); - try { - await packagePolicyService.upgrade(soClient, esClient, [packagePolicyId]); - results.push({ packagePolicyId, diff: dryRunResults.diff, errors: [] }); - } catch (error) { - results.push({ packagePolicyId, diff: dryRunResults.diff, errors: [error] }); + for (const packagePolicy of packagePolicies) { + if (isPolicyVersionLtInstalledVersion(packagePolicy, installedPackage)) { + await upgradePackagePolicy(soClient, esClient, packagePolicy.id, results); } } } - return results; }; -export function shouldUpgradePolicies( - packagePolicyPackageVersion: string, +async function getPackagePoliciesNotMatchingVersion( + soClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string +): Promise { + return ( + await packagePolicyService.list(soClient, { + page: 1, + perPage: 1000, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName} AND NOT ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version:${pkgVersion}`, + }) + ).items; +} + +function isPolicyVersionLtInstalledVersion( + packagePolicy: PackagePolicy, installedPackage: Installation ): boolean { - const isPolicyVersionGteInstalledVersion = semverGte( - packagePolicyPackageVersion, - installedPackage.version + return ( + packagePolicy.package !== undefined && + semverLt(packagePolicy.package.version, installedPackage.version) ); +} + +async function upgradePackagePolicy( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + packagePolicyId: string, + results: UpgradeManagedPackagePoliciesResult[] +) { + // Since upgrades don't report diffs/errors, we need to perform a dry run first in order + // to notify the user of any granular policy upgrade errors that occur during Fleet's + // preconfiguration check + const dryRunResults = await packagePolicyService.getUpgradeDryRunDiff(soClient, packagePolicyId); + + if (dryRunResults.hasErrors) { + const errors = dryRunResults.diff + ? dryRunResults.diff?.[1].errors + : [dryRunResults.body?.message]; + + appContextService + .getLogger() + .error( + new Error(`Error upgrading package policy ${packagePolicyId}: ${JSON.stringify(errors)}`) + ); - return !isPolicyVersionGteInstalledVersion && !!installedPackage.keep_policies_up_to_date; + results.push({ packagePolicyId, diff: dryRunResults.diff, errors }); + return; + } + + try { + await packagePolicyService.upgrade(soClient, esClient, [packagePolicyId]); + results.push({ packagePolicyId, diff: dryRunResults.diff, errors: [] }); + } catch (error) { + results.push({ packagePolicyId, diff: dryRunResults.diff, errors: [error] }); + } } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index e9c079d435e7e3..2e0c3c7722b13d 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -22,7 +22,7 @@ import type { PackagePolicy, } from '../../common'; import { PRECONFIGURATION_LATEST_KEYWORD } from '../../common'; -import { SO_SEARCH_LIMIT, normalizeHostsForAgents } from '../../common'; +import { normalizeHostsForAgents } from '../../common'; import { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE } from '../constants'; import { escapeSearchQueryPhrase } from './saved_object'; @@ -32,10 +32,9 @@ import { ensurePackagesCompletedInstall } from './epm/packages/install'; import { bulkInstallPackages } from './epm/packages/bulk_install_packages'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; import type { InputsOverride } from './package_policy'; -import { preconfigurePackageInputs, packagePolicyService } from './package_policy'; +import { preconfigurePackageInputs } from './package_policy'; import { appContextService } from './app_context'; import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies'; -import { upgradeManagedPackagePolicies } from './managed_package_policies'; import { outputService } from './output'; interface PreconfigurationResult { @@ -357,18 +356,6 @@ export async function ensurePreconfiguredPackagesAndPolicies( } } - // Handle automatic package policy upgrades for managed packages and package with - // the `keep_policies_up_to_date` setting enabled - const allPackagePolicyIds = await packagePolicyService.listIds(soClient, { - page: 1, - perPage: SO_SEARCH_LIMIT, - }); - const packagePolicyUpgradeResults = await upgradeManagedPackagePolicies( - soClient, - esClient, - allPackagePolicyIds.items - ); - return { policies: fulfilledPolicies.map((p) => p.policy @@ -385,7 +372,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( } ), packages: fulfilledPackages.map((pkg) => ('version' in pkg ? pkgToPkgKey(pkg) : pkg.name)), - nonFatalErrors: [...rejectedPackages, ...rejectedPolicies, ...packagePolicyUpgradeResults], + nonFatalErrors: [...rejectedPackages, ...rejectedPolicies], }; } diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index caa2e1b3ffceec..02972648e80d2b 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -5,29 +5,56 @@ * 2.0. */ +import type { SavedObjectsClientContract } from 'kibana/server'; +import type { ElasticsearchClientMock } from 'src/core/server/mocks'; + import { createAppContextStartContractMock, xpackMocks } from '../mocks'; +import { ensurePreconfiguredPackagesAndPolicies } from '.'; + import { appContextService } from './app_context'; +import { getInstallations } from './epm/packages'; +import { upgradeManagedPackagePolicies } from './managed_package_policies'; import { setupFleet } from './setup'; -const mockedMethodThrowsError = () => - jest.fn().mockImplementation(() => { +jest.mock('./preconfiguration'); +jest.mock('./settings'); +jest.mock('./output'); +jest.mock('./epm/packages'); +jest.mock('./managed_package_policies'); + +const mockedMethodThrowsError = (mockFn: jest.Mock) => + mockFn.mockImplementation(() => { throw new Error('SO method mocked to throw'); }); class CustomTestError extends Error {} -const mockedMethodThrowsCustom = () => - jest.fn().mockImplementation(() => { +const mockedMethodThrowsCustom = (mockFn: jest.Mock) => + mockFn.mockImplementation(() => { throw new CustomTestError('method mocked to throw'); }); describe('setupFleet', () => { let context: ReturnType; + let soClient: jest.Mocked; + let esClient: ElasticsearchClientMock; beforeEach(async () => { context = xpackMocks.createRequestHandlerContext(); // prevents `Logger not set.` and other appContext errors appContextService.start(createAppContextStartContractMock()); + soClient = context.core.savedObjects.client; + esClient = context.core.elasticsearch.client.asInternalUser; + + (getInstallations as jest.Mock).mockResolvedValueOnce({ + saved_objects: [], + }); + + (ensurePreconfiguredPackagesAndPolicies as jest.Mock).mockResolvedValue({ + nonFatalErrors: [], + }); + + (upgradeManagedPackagePolicies as jest.Mock).mockResolvedValue([]); }); afterEach(async () => { @@ -37,12 +64,7 @@ describe('setupFleet', () => { describe('should reject with any error thrown underneath', () => { it('SO client throws plain Error', async () => { - const soClient = context.core.savedObjects.client; - soClient.create = mockedMethodThrowsError(); - soClient.find = mockedMethodThrowsError(); - soClient.get = mockedMethodThrowsError(); - soClient.update = mockedMethodThrowsError(); - const esClient = context.core.elasticsearch.client.asInternalUser; + mockedMethodThrowsError(upgradeManagedPackagePolicies as jest.Mock); const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('SO method mocked to throw'); @@ -50,16 +72,53 @@ describe('setupFleet', () => { }); it('SO client throws other error', async () => { - const soClient = context.core.savedObjects.client; - soClient.create = mockedMethodThrowsCustom(); - soClient.find = mockedMethodThrowsCustom(); - soClient.get = mockedMethodThrowsCustom(); - soClient.update = mockedMethodThrowsCustom(); - const esClient = context.core.elasticsearch.client.asInternalUser; + mockedMethodThrowsCustom(upgradeManagedPackagePolicies as jest.Mock); const setupPromise = setupFleet(soClient, esClient); await expect(setupPromise).rejects.toThrow('method mocked to throw'); await expect(setupPromise).rejects.toThrow(CustomTestError); }); }); + + it('should not return non fatal errors when upgrade result has no errors', async () => { + (upgradeManagedPackagePolicies as jest.Mock).mockResolvedValue([ + { + errors: [], + packagePolicyId: '1', + }, + ]); + + const result = await setupFleet(soClient, esClient); + + expect(result).toEqual({ + isInitialized: true, + nonFatalErrors: [], + }); + }); + + it('should return non fatal errors when upgrade result has errors', async () => { + (upgradeManagedPackagePolicies as jest.Mock).mockResolvedValue([ + { + errors: [{ key: 'key', message: 'message' }], + packagePolicyId: '1', + }, + ]); + + const result = await setupFleet(soClient, esClient); + + expect(result).toEqual({ + isInitialized: true, + nonFatalErrors: [ + { + errors: [ + { + key: 'key', + message: 'message', + }, + ], + packagePolicyId: '1', + }, + ], + }); + }); }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index b99b73be8588cb..e7ba627f5cbdfa 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -33,6 +33,7 @@ import { getInstallations, installPackage } from './epm/packages'; import { isPackageInstalled } from './epm/packages/install'; import { pkgToPkgKey } from './epm/registry'; import type { UpgradeManagedPackagePoliciesResult } from './managed_package_policies'; +import { upgradeManagedPackagePolicies } from './managed_package_policies'; export interface SetupStatus { isInitialized: boolean; @@ -98,14 +99,21 @@ async function createSetupSideEffects( logger.debug('Setting up initial Fleet packages'); - const { nonFatalErrors } = await ensurePreconfiguredPackagesAndPolicies( - soClient, - esClient, - policies, - packages, - defaultOutput, - DEFAULT_SPACE_ID - ); + const { nonFatalErrors: preconfiguredPackagesNonFatalErrors } = + await ensurePreconfiguredPackagesAndPolicies( + soClient, + esClient, + policies, + packages, + defaultOutput, + DEFAULT_SPACE_ID + ); + + const packagePolicyUpgradeErrors = ( + await upgradeManagedPackagePolicies(soClient, esClient) + ).filter((result) => (result.errors ?? []).length > 0); + + const nonFatalErrors = [...preconfiguredPackagesNonFatalErrors, ...packagePolicyUpgradeErrors]; logger.debug('Cleaning up Fleet outputs'); await cleanPreconfiguredOutputs(soClient, outputsOrUndefined ?? []); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts index bcce2fa2f6f693..515f5a40fd58af 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts @@ -82,7 +82,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: 'a', }); }); @@ -102,7 +101,6 @@ describe('Table actions', () => { }, ], negate: true, - timeFieldName: 'a', }); }); @@ -122,7 +120,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: 'a', }); }); @@ -142,7 +139,6 @@ describe('Table actions', () => { }, ], negate: true, - timeFieldName: undefined, }); }); }); @@ -173,7 +169,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: 'a', }); }); @@ -202,7 +197,6 @@ describe('Table actions', () => { }, ], negate: true, - timeFieldName: undefined, }); }); @@ -274,7 +268,6 @@ describe('Table actions', () => { }, ], negate: false, - timeFieldName: undefined, }); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts index 3c1297e8645532..c37ab22002c1ce 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -75,10 +75,6 @@ export const createGridFilterHandler = onClickValue: (data: LensFilterEvent['data']) => void ) => (field: string, value: unknown, colIndex: number, rowIndex: number, negate: boolean = false) => { - const col = tableRef.current.columns[colIndex]; - const isDate = col.meta?.type === 'date'; - const timeFieldName = negate && isDate ? undefined : col?.meta?.field; - const data: LensFilterEvent['data'] = { negate, data: [ @@ -89,7 +85,6 @@ export const createGridFilterHandler = table: tableRef.current, }, ], - timeFieldName, }; onClickValue(data); @@ -106,11 +101,6 @@ export const createTransposeColumnFilterHandler = ) => { if (!untransposedDataRef.current) return; const originalTable = Object.values(untransposedDataRef.current.tables)[0]; - const timeField = bucketValues.find( - ({ originalBucketColumn }) => originalBucketColumn.meta.type === 'date' - )?.originalBucketColumn; - const isDate = Boolean(timeField); - const timeFieldName = negate && isDate ? undefined : timeField?.meta?.field; const data: LensFilterEvent['data'] = { negate, @@ -126,7 +116,6 @@ export const createTransposeColumnFilterHandler = table: originalTable, }; }), - timeFieldName, }; onClickValue(data); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 8c75ee9efcc6bb..6cab22cd08ccdf 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -210,7 +210,6 @@ describe('DatatableComponent', () => { }, ], negate: true, - timeFieldName: 'a', }, }); }); @@ -256,7 +255,6 @@ describe('DatatableComponent', () => { }, ], negate: false, - timeFieldName: 'b', }, }); }); @@ -341,7 +339,6 @@ describe('DatatableComponent', () => { }, ], negate: false, - timeFieldName: 'a', }, }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 5d475be7bb83fe..ccd9e8aace2ab2 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -204,11 +204,11 @@ describe('workspace_panel', () => { const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; - const eventData = {}; + const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 }; onEvent({ name: 'brush', data: eventData }); expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); - expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); + expect(trigger.exec).toHaveBeenCalledWith({ data: { ...eventData, timeFieldName: undefined } }); }); it('should push add current data table to state on data$ emitting value', async () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 3554f770475772..a26d72f1b4fc2d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -68,6 +68,7 @@ import { selectSearchSessionId, } from '../../../state_management'; import type { LensInspector } from '../../../lens_inspector_service'; +import { inferTimeField } from '../../../utils'; export interface WorkspacePanelProps { visualizationMap: VisualizationMap; @@ -250,12 +251,18 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ } if (isLensBrushEvent(event)) { plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: inferTimeField(event.data), + }, }); } if (isLensFilterEvent(event)) { plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: inferTimeField(event.data), + }, }); } if (isLensEditEvent(event) && activeVisualization?.onEditAction) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 5ae3cb571bdbb0..c1c86367ee2118 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -821,12 +821,15 @@ describe('embeddable', () => { const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; - const eventData = {}; + const eventData = { myData: true, table: { rows: [], columns: [] }, column: 0 }; onEvent({ name: 'brush', data: eventData }); expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); expect(trigger.exec).toHaveBeenCalledWith( - expect.objectContaining({ data: eventData, embeddable: expect.anything() }) + expect.objectContaining({ + data: { ...eventData, timeFieldName: undefined }, + embeddable: expect.anything(), + }) ); }); @@ -1006,7 +1009,10 @@ describe('embeddable', () => { expressionRenderer = jest.fn(({ onEvent }) => { setTimeout(() => { - onEvent?.({ name: 'filter', data: { pings: false } }); + onEvent?.({ + name: 'filter', + data: { pings: false, table: { rows: [], columns: [] }, column: 0 }, + }); }, 10); return null; @@ -1048,7 +1054,7 @@ describe('embeddable', () => { await new Promise((resolve) => setTimeout(resolve, 20)); - expect(onFilter).toHaveBeenCalledWith({ pings: false }); + expect(onFilter).toHaveBeenCalledWith(expect.objectContaining({ pings: false })); expect(onFilter).toHaveBeenCalledTimes(1); }); @@ -1057,7 +1063,10 @@ describe('embeddable', () => { expressionRenderer = jest.fn(({ onEvent }) => { setTimeout(() => { - onEvent?.({ name: 'brush', data: { range: [0, 1] } }); + onEvent?.({ + name: 'brush', + data: { range: [0, 1], table: { rows: [], columns: [] }, column: 0 }, + }); }, 10); return null; @@ -1099,7 +1108,7 @@ describe('embeddable', () => { await new Promise((resolve) => setTimeout(resolve, 20)); - expect(onBrushEnd).toHaveBeenCalledWith({ range: [0, 1] }); + expect(onBrushEnd).toHaveBeenCalledWith(expect.objectContaining({ range: [0, 1] })); expect(onBrushEnd).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 712e9f9f7f4769..aa0a9de248c1b3 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; import { Filter } from '@kbn/es-query'; -import type { +import { ExecutionContextSearch, Query, TimefilterContract, @@ -70,6 +70,7 @@ import type { ErrorMessage } from '../editor_frame_service/types'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; import { SharingSavedObjectProps } from '../types'; import type { SpacesPluginStart } from '../../../spaces/public'; +import { inferTimeField } from '../utils'; export type LensSavedObjectAttributes = Omit; @@ -529,7 +530,10 @@ export class Embeddable } if (isLensBrushEvent(event)) { this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: event.data.timeFieldName || inferTimeField(event.data), + }, embeddable: this, }); @@ -539,7 +543,10 @@ export class Embeddable } if (isLensFilterEvent(event)) { this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ - data: event.data, + data: { + ...event.data, + timeFieldName: event.data.timeFieldName || inferTimeField(event.data), + }, embeddable: this, }); if (this.input.onFilter) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 404c31010278b3..2c74f8468e52bb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -491,13 +491,13 @@ describe('IndexPattern Data Source', () => { `); }); - it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { + it('should put all time fields used in date_histograms to the esaggs timeFields parameter if not ignoring global time range', async () => { const queryBaseState: DataViewBaseState = { currentIndexPatternId: '1', layers: { first: { indexPatternId: '1', - columnOrder: ['col1', 'col2', 'col3'], + columnOrder: ['col1', 'col2', 'col3', 'col4'], columns: { col1: { label: 'Count of records', @@ -526,6 +526,17 @@ describe('IndexPattern Data Source', () => { interval: 'auto', }, } as DateHistogramIndexPatternColumn, + col4: { + label: 'Date 3', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'yet_another_datefield', + params: { + interval: '2d', + ignoreTimeRange: true, + }, + } as DateHistogramIndexPatternColumn, }, }, }, @@ -1633,6 +1644,63 @@ describe('IndexPattern Data Source', () => { }); expect(indexPatternDatasource.isTimeBased(state)).toEqual(true); }); + it('should return false if date histogram exists but is detached from global time range in every layer', () => { + const state = enrichBaseState({ + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['metric'], + columns: { + metric: { + label: 'Count of records2', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + }, + }, + second: { + indexPatternId: '1', + columnOrder: ['bucket1', 'bucket2', 'metric2'], + columns: { + metric2: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: '___records___', + operationType: 'count', + }, + bucket1: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + ignoreTimeRange: true, + }, + } as DateHistogramIndexPatternColumn, + bucket2: { + label: 'Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'geo.src', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 10, + }, + } as TermsIndexPatternColumn, + }, + }, + }, + }); + expect(indexPatternDatasource.isTimeBased(state)).toEqual(false); + }); it('should return false if date histogram does not exist in any layer', () => { const state = enrichBaseState({ currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2a44550af2b588..0ac77696d5987c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -48,7 +48,12 @@ import { import { getVisualDefaultsForLayer, isColumnInvalid } from './utils'; import { normalizeOperationDataType, isDraggedField } from './pure_utils'; import { LayerPanel } from './layerpanel'; -import { GenericIndexPatternColumn, getErrorMessages, insertNewColumn } from './operations'; +import { + DateHistogramIndexPatternColumn, + GenericIndexPatternColumn, + getErrorMessages, + insertNewColumn, +} from './operations'; import { IndexPatternField, IndexPatternPrivateState, @@ -70,6 +75,7 @@ import { GeoFieldWorkspacePanel } from '../editor_frame_service/editor_frame/wor import { DraggingIdentifier } from '../drag_drop'; import { getStateTimeShiftWarningMessages } from './time_shift_utils'; import { getPrecisionErrorWarningMessages } from './utils'; +import { isColumnOfType } from './operations/definitions/helpers'; export type { OperationType, GenericIndexPatternColumn } from './operations'; export { deleteColumn } from './operations'; @@ -561,7 +567,13 @@ export function getIndexPatternDatasource({ Boolean(layers) && Object.values(layers).some((layer) => { const buckets = layer.columnOrder.filter((colId) => layer.columns[colId].isBucketed); - return buckets.some((colId) => layer.columns[colId].operationType === 'date_histogram'); + return buckets.some((colId) => { + const column = layer.columns[colId]; + return ( + isColumnOfType('date_histogram', column) && + !column.params.ignoreTimeRange + ); + }); }) ); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index 26cbd2a9909780..beca7cfa4c39f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -423,6 +423,92 @@ describe('date_histogram', () => { expect(newLayer).toHaveProperty('columns.col1.params.interval', '30d'); }); + it('should allow turning off time range sync', () => { + const thirdLayer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1h', + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + }, + }; + + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + instance + .find(EuiSwitch) + .at(1) + .simulate('change', { + target: { checked: false }, + }); + expect(updateLayerSpy).toHaveBeenCalled(); + const newLayer = updateLayerSpy.mock.calls[0][0]; + expect(newLayer).toHaveProperty('columns.col1.params.ignoreTimeRange', true); + }); + + it('turns off time range ignore on switching to auto interval', () => { + const thirdLayer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1h', + ignoreTimeRange: true, + }, + sourceField: 'timestamp', + } as DateHistogramIndexPatternColumn, + }, + }; + + const updateLayerSpy = jest.fn(); + const instance = shallow( + + ); + instance + .find(EuiSwitch) + .at(0) + .simulate('change', { + target: { checked: false }, + }); + expect(updateLayerSpy).toHaveBeenCalled(); + const newLayer = updateLayerSpy.mock.calls[0][0]; + expect(newLayer).toHaveProperty('columns.col1.params.ignoreTimeRange', false); + expect(newLayer).toHaveProperty('columns.col1.params.interval', 'auto'); + }); + it('should force calendar values to 1', () => { const updateLayerSpy = jest.fn(); const instance = shallow( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx index ea43766464cf58..e269778b5ad53e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx @@ -16,6 +16,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiIconTip, EuiSelect, EuiSpacer, EuiSwitch, @@ -46,6 +47,7 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC operationType: 'date_histogram'; params: { interval: string; + ignoreTimeRange?: boolean; }; } @@ -189,7 +191,14 @@ export const dateHistogramOperation: OperationDefinition< const value = ev.target.checked ? data.search.aggs.calculateAutoTimeExpression({ from: fromDate, to: toDate }) || '1h' : autoInterval; - updateLayer(updateColumnParam({ layer, columnId, paramName: 'interval', value })); + updateLayer( + updateColumnParam({ + layer: updateColumnParam({ layer, columnId, paramName: 'interval', value }), + columnId, + paramName: 'ignoreTimeRange', + value: false, + }) + ); } const setInterval = (newInterval: typeof interval) => { @@ -214,128 +223,176 @@ export const dateHistogramOperation: OperationDefinition< )} {currentColumn.params.interval !== autoInterval && ( - - {intervalIsRestricted ? ( - - ) : ( - <> - - - + + {intervalIsRestricted ? ( + + ) : ( + <> + + + { + const newInterval = { + ...interval, + value: e.target.value, + }; + setInterval(newInterval); + }} + step={1} + /> + + + { + const newInterval = { + ...interval, + unit: e.target.value, + }; + setInterval(newInterval); + }} + isInvalid={!isValid} + options={[ + { + value: 'ms', + text: i18n.translate( + 'xpack.lens.indexPattern.dateHistogram.milliseconds', + { + defaultMessage: 'milliseconds', + } + ), + }, + { + value: 's', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.seconds', { + defaultMessage: 'seconds', + }), + }, + { + value: 'm', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.minutes', { + defaultMessage: 'minutes', + }), + }, + { + value: 'h', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.hours', { + defaultMessage: 'hours', + }), + }, + { + value: 'd', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.days', { + defaultMessage: 'days', + }), + }, + { + value: 'w', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.week', { + defaultMessage: 'week', + }), + }, + { + value: 'M', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.month', { + defaultMessage: 'month', + }), + }, + // Quarterly intervals appear to be unsupported by esaggs + { + value: 'y', + text: i18n.translate('xpack.lens.indexPattern.dateHistogram.year', { + defaultMessage: 'year', + }), + }, + ]} + /> + + + {!isValid && ( + <> + + + {i18n.translate('xpack.lens.indexPattern.invalidInterval', { + defaultMessage: 'Invalid interval value', + })} + + + )} + + )} + + + + {i18n.translate( + 'xpack.lens.indexPattern.dateHistogram.bindToGlobalTimePicker', + { + defaultMessage: 'Bind to global time picker', } - disabled={calendarOnlyIntervals.has(interval.unit)} - isInvalid={!isValid} - onChange={(e) => { - const newInterval = { - ...interval, - value: e.target.value, - }; - setInterval(newInterval); - }} - step={1} - /> - - - { - const newInterval = { - ...interval, - unit: e.target.value, - }; - setInterval(newInterval); - }} - isInvalid={!isValid} - options={[ - { - value: 'ms', - text: i18n.translate( - 'xpack.lens.indexPattern.dateHistogram.milliseconds', - { - defaultMessage: 'milliseconds', - } - ), - }, - { - value: 's', - text: i18n.translate('xpack.lens.indexPattern.dateHistogram.seconds', { - defaultMessage: 'seconds', - }), - }, + )}{' '} + - - - {!isValid && ( - <> - - - {i18n.translate('xpack.lens.indexPattern.invalidInterval', { - defaultMessage: 'Invalid interval value', - })} - - )} - - )} - + } + disabled={indexPattern.timeFieldName === field?.name} + checked={ + indexPattern.timeFieldName === field?.name || + !currentColumn.params.ignoreTimeRange + } + onChange={() => { + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'ignoreTimeRange', + value: !currentColumn.params.ignoreTimeRange, + }) + ); + }} + compressed + /> + + )} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 0d8b57a5502ad5..f9fe8701949e18 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -263,7 +263,8 @@ function getExpressionForLayer( const allDateHistogramFields = Object.values(columns) .map((column) => - isColumnOfType('date_histogram', column) + isColumnOfType('date_histogram', column) && + !column.params.ignoreTimeRange ? column.sourceField : null ) diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts new file mode 100644 index 00000000000000..857f30e692305e --- /dev/null +++ b/x-pack/plugins/lens/public/utils.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable } from 'src/plugins/expressions/public'; +import { inferTimeField } from './utils'; + +const table: Datatable = { + type: 'datatable', + rows: [], + columns: [ + { + id: '1', + name: '', + meta: { + type: 'date', + field: 'abc', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2021-01-01', + to: '2022-01-01', + }, + }, + }, + }, + ], +}; + +const tableWithoutAppliedTimeRange = { + ...table, + columns: [ + { + ...table.columns[0], + meta: { + ...table.columns[0].meta, + sourceParams: { + ...table.columns[0].meta.sourceParams, + appliedTimeRange: undefined, + }, + }, + }, + ], +}; + +describe('utils', () => { + describe('inferTimeField', () => { + test('infer time field for brush event', () => { + expect( + inferTimeField({ + table, + column: 0, + range: [1, 2], + }) + ).toEqual('abc'); + }); + + test('do not return time field if time range is not bound', () => { + expect( + inferTimeField({ + table: tableWithoutAppliedTimeRange, + column: 0, + range: [1, 2], + }) + ).toEqual(undefined); + }); + + test('infer time field for click event', () => { + expect( + inferTimeField({ + data: [ + { + table, + column: 0, + row: 0, + value: 1, + }, + ], + }) + ).toEqual('abc'); + }); + + test('do not return time field for negated click event', () => { + expect( + inferTimeField({ + data: [ + { + table, + column: 0, + row: 0, + value: 1, + }, + ], + negate: true, + }) + ).toEqual(undefined); + }); + + test('do not return time field for click event without bound time field', () => { + expect( + inferTimeField({ + data: [ + { + table: tableWithoutAppliedTimeRange, + column: 0, + row: 0, + value: 1, + }, + ], + }) + ).toEqual(undefined); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 921cc8fb364a26..f71f7a128934aa 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -16,7 +16,14 @@ import type { import type { IUiSettingsClient } from 'kibana/public'; import type { SavedObjectReference } from 'kibana/public'; import type { Document } from './persistence/saved_object_store'; -import type { Datasource, DatasourceMap, Visualization } from './types'; +import type { + Datasource, + DatasourceMap, + LensBrushEvent, + LensFilterEvent, + Visualization, +} from './types'; +import { search } from '../../../../src/plugins/data/public'; import type { DatasourceStates, VisualizationState } from './state_management'; export function getVisualizeGeoFieldMessage(fieldType: string) { @@ -107,3 +114,24 @@ export function getRemoveOperation( // fallback to generic count check return layerCount === 1 ? 'clear' : 'remove'; } + +export function inferTimeField(context: LensBrushEvent['data'] | LensFilterEvent['data']) { + const tablesAndColumns = + 'table' in context + ? [{ table: context.table, column: context.column }] + : !context.negate + ? context.data + : // if it's a negated filter, never respect bound time field + []; + return tablesAndColumns + .map(({ table, column }) => { + const tableColumn = table.columns[column]; + const hasTimeRange = Boolean( + tableColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(tableColumn)?.timeRange + ); + if (hasTimeRange) { + return tableColumn.meta.field; + } + }) + .find(Boolean); +} diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index bc57547bc0ee66..6bee021b36de66 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -134,6 +134,10 @@ const dateHistogramData: LensMultiTable = { sourceParams: { indexPatternId: 'indexPatternId', type: 'date_histogram', + appliedTimeRange: { + from: '2020-04-01T16:14:16.246Z', + to: '2020-04-01T17:15:41.263Z', + }, params: { field: 'order_date', timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, @@ -582,9 +586,29 @@ describe('xy_expression', () => { {...defaultProps} data={{ ...data, - dateRange: { - fromDate: new Date('2019-01-02T05:00:00.000Z'), - toDate: new Date('2019-01-03T05:00:00.000Z'), + tables: { + first: { + ...data.tables.first, + columns: data.tables.first.columns.map((c) => + c.id !== 'c' + ? c + : { + ...c, + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2019-01-02T05:00:00.000Z', + to: '2019-01-03T05:00:00.000Z', + }, + }, + }, + } + ), + }, }, }} args={{ @@ -612,25 +636,13 @@ describe('xy_expression', () => { }, }; - const component = shallow( - - ); + const component = shallow(); // real auto interval is 30mins = 1800000 expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` Object { - "max": 1546491600000, - "min": 1546405200000, + "max": NaN, + "min": NaN, "minInterval": 50, } `); @@ -749,14 +761,36 @@ describe('xy_expression', () => { }); describe('endzones', () => { const { args } = sampleArgs(); + const table = createSampleDatatableWithRows([ + { a: 1, b: 2, c: new Date('2021-04-22').valueOf(), d: 'Foo' }, + { a: 1, b: 2, c: new Date('2021-04-23').valueOf(), d: 'Foo' }, + { a: 1, b: 2, c: new Date('2021-04-24').valueOf(), d: 'Foo' }, + ]); const data: LensMultiTable = { type: 'lens_multitable', tables: { - first: createSampleDatatableWithRows([ - { a: 1, b: 2, c: new Date('2021-04-22').valueOf(), d: 'Foo' }, - { a: 1, b: 2, c: new Date('2021-04-23').valueOf(), d: 'Foo' }, - { a: 1, b: 2, c: new Date('2021-04-24').valueOf(), d: 'Foo' }, - ]), + first: { + ...table, + columns: table.columns.map((c) => + c.id !== 'c' + ? c + : { + ...c, + meta: { + type: 'date', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + params: {}, + appliedTimeRange: { + from: '2021-04-22T12:00:00.000Z', + to: '2021-04-24T12:00:00.000Z', + }, + }, + }, + } + ), + }, }, dateRange: { // first and last bucket are partial @@ -1187,7 +1221,6 @@ describe('xy_expression', () => { column: 0, table: dateHistogramData.tables.timeLayer, range: [1585757732783, 1585758880838], - timeFieldName: 'order_date', }); }); @@ -1267,7 +1300,6 @@ describe('xy_expression', () => { column: 0, table: numberHistogramData.tables.numberLayer, range: [5, 8], - timeFieldName: undefined, }); }); @@ -1398,7 +1430,6 @@ describe('xy_expression', () => { value: 1585758120000, }, ], - timeFieldName: 'order_date', }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 9594d8920515b7..ea0e336ff2f08a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -106,7 +106,7 @@ export type XYChartRenderProps = XYChartProps & { export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) return; - const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); + const isTimeViz = filteredLayers.every((l) => l.xScaleType === 'time'); const xColumn = data.tables[filteredLayers[0].layerId].columns.find( (column) => column.id === filteredLayers[0].xAccessor ); @@ -315,7 +315,7 @@ export function XYChart({ filteredBarLayers.some((layer) => layer.accessors.length > 1) || filteredBarLayers.some((layer) => layer.splitAccessor); - const isTimeViz = Boolean(data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time')); + const isTimeViz = Boolean(filteredLayers.every((l) => l.xScaleType === 'time')); const isHistogramViz = filteredLayers.every((l) => l.isHistogram); const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( @@ -512,10 +512,6 @@ export function XYChart({ value: pointValue, }); } - const currentColumnMeta = table.columns.find((el) => el.id === layer.xAccessor)?.meta; - const xAxisFieldName = currentColumnMeta?.field; - const isDateField = currentColumnMeta?.type === 'date'; - const context: LensFilterEvent['data'] = { data: points.map((point) => ({ row: point.row, @@ -523,7 +519,6 @@ export function XYChart({ value: point.value, table, })), - timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined, }; onClickValue(context); }; @@ -541,13 +536,10 @@ export function XYChart({ const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor); - const timeFieldName = isTimeViz ? table.columns[xAxisColumnIndex]?.meta?.field : undefined; - const context: LensBrushEvent['data'] = { range: [min, max], table, column: xAxisColumnIndex, - timeFieldName, }; onSelectRange(context); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx b/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx index d5eb8ac1e92ba7..81037418a8143c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx @@ -7,9 +7,11 @@ import { uniq } from 'lodash'; import React from 'react'; +import moment from 'moment'; import { Endzones } from '../../../../../src/plugins/charts/public'; import type { LensMultiTable } from '../../common'; import type { LayerArgs } from '../../common/expressions'; +import { search } from '../../../../../src/plugins/data/public'; export interface XDomain { min?: number; @@ -17,6 +19,23 @@ export interface XDomain { minInterval?: number; } +export const getAppliedTimeRange = (layers: LayerArgs[], data: LensMultiTable) => { + return Object.entries(data.tables) + .map(([tableId, table]) => { + const layer = layers.find((l) => l.layerId === tableId); + const xColumn = table.columns.find((col) => col.id === layer?.xAccessor); + const timeRange = + xColumn && search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.timeRange; + if (timeRange) { + return { + timeRange, + field: xColumn.meta.field, + }; + } + }) + .find(Boolean); +}; + export const getXDomain = ( layers: LayerArgs[], data: LensMultiTable, @@ -24,10 +43,13 @@ export const getXDomain = ( isTimeViz: boolean, isHistogram: boolean ) => { + const appliedTimeRange = getAppliedTimeRange(layers, data)?.timeRange; + const from = appliedTimeRange?.from; + const to = appliedTimeRange?.to; const baseDomain = isTimeViz ? { - min: data.dateRange?.fromDate.getTime() ?? NaN, - max: data.dateRange?.toDate.getTime() ?? NaN, + min: from ? moment(from).valueOf() : NaN, + max: to ? moment(to).valueOf() : NaN, minInterval, } : isHistogram diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/README.md b/x-pack/plugins/reporting/server/export_types/common/pdf/README.md new file mode 100644 index 00000000000000..d2536605f5b384 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/README.md @@ -0,0 +1,54 @@ +# Use of worker threads + +See following performance characteristics of generating a PDF buffer +using a worker thread, for a given, small PDF report. TL;DR running this code for +a small report in a release build is _far_ more performant and takes about 20% +of the time of dev builds. + +### Dev build: new worker for each run + transpile from TS (seconds) + +``` +3.885 +3.063 +2.64 +2.821 +``` + +### Release build: new worker for each run + no transpile from TS (seconds) + +``` +0.674 +0.77 +0.712 +0.77 +``` + +Transpiling TS code is expensive (very small reports can take up to 5x longer). +However, release builds ship all JS which is far more performant for generating +PDF buffers. + +### Use of long-lived workers + +One alternative that was investigated is use of long-lived workers. This would +mean re-using a single worker over time for making a PDF buffer. The following +performance was observed for dev and release builds on non-initial runs that did +not instantiate a new worker: + +``` +0.328 +0.332 +0.368 +0.328 +0.341 +0.358 +0.316 +0.257 +0.378 +0.326 +``` + +Clearly there is overhead for just instantiating a worker thread. We decided to +avoid long-lived workers for our initial implementation since, even though it is +about ~50% extra time the overhead for small reports this number (~0.3s) will +be proportionally far smaller for larger, more common PDFs. That take longer +to compile. diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/constants.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/constants.ts new file mode 100644 index 00000000000000..7e20d1d8b45d5f --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/constants.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. + */ + +import path from 'path'; + +export const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets'); +export const tableBorderWidth = 1; +export const pageMarginTop = 40; +export const pageMarginBottom = 80; +export const pageMarginWidth = 40; +export const headingFontSize = 14; +export const headingMarginTop = 10; +export const headingMarginBottom = 5; +export const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; +export const subheadingFontSize = 12; +export const subheadingMarginTop = 0; +export const subheadingMarginBottom = 5; +export const subheadingHeight = + subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts index 0c7fedc8f7b7ec..80c5238411379e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts @@ -12,30 +12,29 @@ import { DynamicContent, StyleDictionary, TDocumentDefinitions, + PredefinedPageSize, } from 'pdfmake/interfaces'; -import type { Layout } from '../../../../../screenshotting/server'; import { REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; +import { TemplateLayout } from './types'; +import { + headingFontSize, + headingMarginBottom, + headingMarginTop, + pageMarginBottom, + pageMarginTop, + pageMarginWidth, + subheadingFontSize, + subheadingMarginBottom, +} from './constants'; export function getTemplate( - layout: Layout, + layout: TemplateLayout, logo: string | undefined, title: string, tableBorderWidth: number, assetPath: string ): Partial { - const pageMarginTop = 40; - const pageMarginBottom = 80; - const pageMarginWidth = 40; - const headingFontSize = 14; - const headingMarginTop = 10; - const headingMarginBottom = 5; - const headingHeight = headingFontSize * 1.5 + headingMarginTop + headingMarginBottom; - const subheadingFontSize = 12; - const subheadingMarginTop = 0; - const subheadingMarginBottom = 5; - const subheadingHeight = subheadingFontSize * 1.5 + subheadingMarginTop + subheadingMarginBottom; - const getStyle = (): StyleDictionary => ({ heading: { alignment: 'left', @@ -111,15 +110,8 @@ export function getTemplate( return { // define page size - pageOrientation: layout.getPdfPageOrientation(), - pageSize: layout.getPdfPageSize({ - pageMarginTop, - pageMarginBottom, - pageMarginWidth, - tableBorderWidth, - headingHeight, - subheadingHeight, - }), + pageOrientation: layout.orientation, + pageSize: layout.pageSize as PredefinedPageSize, pageMargins: layout.useReportingBranding ? [pageMarginWidth, pageMarginTop, pageMarginWidth, pageMarginBottom] : [0, 0, 0, 0], diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts index e4a1680a958dd2..1f7f2e21a45a27 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts @@ -6,3 +6,4 @@ */ export { PdfMaker } from './pdfmaker'; +export { PdfWorkerOutOfMemoryError } from './pdfmaker_errors'; diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/buggy_worker.js b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/buggy_worker.js new file mode 100644 index 00000000000000..47397f2d34a1f5 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/buggy_worker.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +(async function execute() { + throw new Error('This is a bug'); +})(); diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/memory_leak_worker.js b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/memory_leak_worker.js new file mode 100644 index 00000000000000..1064daadf68780 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/memory_leak_worker.js @@ -0,0 +1,31 @@ +/* + * 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. + */ +const { isMainThread, resourceLimits } = require('worker_threads'); + +// Give Node.js a chance to move the memory to the old generation region +const WAIT = 40; + +const allocateMemory = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + new Array(resourceLimits.maxYoungGenerationSizeMb * 1024 * 1024) + .fill('') + .map((_, idx) => idx) // more unique values prevent aggressive memory compression and hits mem limits faster + ); + }, WAIT); + }); +}; + +if (!isMainThread) { + (async function run() { + const memoryLeak = []; + for (;;) /* a computer crying */ { + memoryLeak.push(await allocateMemory()); + } + })(); +} diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts index 4258349726ccf7..4b35e0221685be 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/integration_tests/pdfmaker.test.ts @@ -5,15 +5,20 @@ * 2.0. */ +/* eslint-disable max-classes-per-file */ + +import path from 'path'; +import { isUint8Array } from 'util/types'; import { createMockLayout } from '../../../../../../screenshotting/server/layouts/mock'; import { PdfMaker } from '../'; +import { PdfWorkerOutOfMemoryError } from '../pdfmaker_errors'; const imageBase64 = Buffer.from( `iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAGFBMVEXy8vJpaWn7+/vY2Nj39/cAAACcnJzx8fFvt0oZAAAAi0lEQVR4nO3SSQoDIBBFwR7U3P/GQXKEIIJULXr9H3TMrHhX5Yysvj3jjM8+XRnVa9wec8QuHKv3h74Z+PNyGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/xu3Bxy026rXu4ljdUVW395xUFfGzLo946DK+QW+bgCTFcecSAAAAABJRU5ErkJggg==`, 'base64' ); -describe('PdfMaker', () => { +describe.skip('PdfMaker', () => { let layout: ReturnType; let pdf: PdfMaker; @@ -22,14 +27,44 @@ describe('PdfMaker', () => { pdf = new PdfMaker(layout, undefined); }); - describe('getBuffer', () => { - it('should generate PDF buffer', async () => { + describe('generate', () => { + it('should generate PDF array buffer', async () => { pdf.setTitle('the best PDF in the world'); pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }); pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }); - pdf.generate(); - await expect(pdf.getBuffer()).resolves.toBeInstanceOf(Buffer); + expect(isUint8Array(await pdf.generate())).toBe(true); + }); + }); + + describe('worker', () => { + /** + * Leave this test skipped! It is a proof-of-concept for demonstrating that + * we correctly handle a worker OOM error. Due to the variability of when + * Node will terminate the worker thread for exceeding resource + * limits we cannot guarantee this test will always execute in a reasonable + * amount of time. + */ + it.skip('should report when the PDF worker runs out of memory instead of crashing the main thread', async () => { + const leakyMaker = new (class MemoryLeakPdfMaker extends PdfMaker { + // From local testing: + // OOMs after 456.486 seconds with high young generation size + // OOMs after 53.538 seconds low young generation size + protected workerMaxOldHeapSizeMb = 2; + protected workerMaxYoungHeapSizeMb = 2; + protected workerModulePath = path.resolve(__dirname, './memory_leak_worker.js'); + })(layout, undefined); + await expect(leakyMaker.generate()).rejects.toBeInstanceOf(PdfWorkerOutOfMemoryError); + }); + + it.skip('restarts the PDF worker if it crashes', async () => { + const buggyMaker = new (class BuggyPdfMaker extends PdfMaker { + protected workerModulePath = path.resolve(__dirname, './buggy_worker.js'); + })(layout, undefined); + + await expect(buggyMaker.generate()).rejects.toEqual(new Error('This is a bug')); + await expect(buggyMaker.generate()).rejects.toEqual(new Error('This is a bug')); + await expect(buggyMaker.generate()).rejects.toEqual(new Error('This is a bug')); }); }); @@ -38,11 +73,11 @@ describe('PdfMaker', () => { expect(pdf.getPageCount()).toBe(0); }); - it('should return a number of generated pages', () => { + it('should return a number of generated pages', async () => { for (let i = 0; i < 100; i++) { pdf.addImage(imageBase64, { title: `${i} viz`, description: '☃️' }); } - pdf.generate(); + await pdf.generate(); expect(pdf.getPageCount()).toBe(100); }); @@ -51,8 +86,7 @@ describe('PdfMaker', () => { for (let i = 0; i < 100; i++) { pdf.addImage(imageBase64, { title: `${i} viz`, description: '☃️' }); } - pdf.generate(); - await pdf.getBuffer(); + await pdf.generate(); expect(pdf.getPageCount()).toBe(100); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts index 4d462a429607a0..18e5346f71c40d 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker.ts @@ -5,52 +5,69 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -// @ts-ignore: no module definition -import concat from 'concat-stream'; -import _ from 'lodash'; +import { SerializableRecord } from '@kbn/utility-types'; import path from 'path'; -import Printer from 'pdfmake'; import { Content, ContentImage, ContentText } from 'pdfmake/interfaces'; +import { MessageChannel, MessagePort, Worker } from 'worker_threads'; import type { Layout } from '../../../../../screenshotting/server'; -import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options'; +import { + headingHeight, + pageMarginBottom, + pageMarginTop, + pageMarginWidth, + subheadingHeight, + tableBorderWidth, +} from './constants'; +import { REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; -import { getTemplate } from './get_template'; +import { PdfWorkerOutOfMemoryError } from './pdfmaker_errors'; +import type { GeneratePdfRequest, GeneratePdfResponse, WorkerData } from './worker'; -const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets'); -const tableBorderWidth = 1; +// Ensure that all dependencies are included in the release bundle. +import './worker_dependencies'; export class PdfMaker { - private _layout: Layout; + _layout: Layout; private _logo: string | undefined; private _title: string; private _content: Content[]; - private _printer: Printer; - private _pdfDoc: PDFKit.PDFDocument | undefined; - constructor(layout: Layout, logo: string | undefined) { - const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); - const fonts = { - Roboto: { - normal: fontPath('roboto/Roboto-Regular.ttf'), - bold: fontPath('roboto/Roboto-Medium.ttf'), - italics: fontPath('roboto/Roboto-Italic.ttf'), - bolditalics: fontPath('roboto/Roboto-Italic.ttf'), - }, - 'noto-cjk': { - // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. - normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), - bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), - }, - }; + private worker?: Worker; + private pageCount: number = 0; + + protected workerModulePath = path.resolve(__dirname, './worker.js'); + + /** + * The maximum heap size for old memory region of the worker thread. + * + * @note We need to provide a sane number given that we need to load a + * node environment for TS compilation (dev-builds only), some library code + * and buffers that result from generating a PDF. + * + * Local testing indicates that to trigger an OOM event for the worker we need + * to exhaust not only heap but also any compression optimization and fallback + * swap space. + * + * With this value we are able to generate PDFs in excess of 5000x5000 pixels + * at which point issues other than memory start to show like glitches in the + * image. + */ + protected workerMaxOldHeapSizeMb = 128; + + /** + * The maximum heap size for young memory region of the worker thread. + * + * @note we leave this 'undefined' to use the Node.js default value. + * @note we set this to a low value to trigger an OOM event sooner for the worker + * in test scenarios. + */ + protected workerMaxYoungHeapSizeMb: number | undefined = undefined; + constructor(layout: Layout, logo: string | undefined) { this._layout = layout; this._logo = logo; this._title = ''; this._content = []; - this._printer = new Printer(fonts); } _addContents(contents: Content[]) { @@ -124,47 +141,91 @@ export class PdfMaker { this._title = title; } - generate() { - const docTemplate = _.assign( - getTemplate(this._layout, this._logo, this._title, tableBorderWidth, assetPath), - { - content: this._content, - } - ); - this._pdfDoc = this._printer.createPdfKitDocument(docTemplate, getDocOptions(tableBorderWidth)); - return this; + private getGeneratePdfRequestData(): GeneratePdfRequest['data'] { + return { + layout: { + hasFooter: this._layout.hasFooter, + hasHeader: this._layout.hasHeader, + orientation: this._layout.getPdfPageOrientation(), + useReportingBranding: this._layout.useReportingBranding, + pageSize: this._layout.getPdfPageSize({ + pageMarginTop, + pageMarginBottom, + pageMarginWidth, + tableBorderWidth, + headingHeight, + subheadingHeight, + }), + }, + title: this._title, + logo: this._logo, + content: this._content as unknown as SerializableRecord[], + }; } - getBuffer(): Promise { - return new Promise((resolve, reject) => { - if (!this._pdfDoc) { - throw new Error( - i18n.translate( - 'xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage', - { - defaultMessage: 'Document stream has not been generated', - } - ) - ); - } - - const concatStream = concat(function (pdfBuffer: Buffer) { - resolve(pdfBuffer); - }); - - this._pdfDoc.on('error', reject); - this._pdfDoc.pipe(concatStream); - this._pdfDoc.end(); + private createWorker(port: MessagePort): Worker { + const workerData: WorkerData = { + port, + }; + return new Worker(this.workerModulePath, { + workerData, + resourceLimits: { + maxYoungGenerationSizeMb: this.workerMaxYoungHeapSizeMb, + maxOldGenerationSizeMb: this.workerMaxOldHeapSizeMb, + }, + transferList: [port], }); } - getPageCount(): number { - const pageRange = this._pdfDoc?.bufferedPageRange(); - if (!pageRange) { - return 0; + private async cleanupWorker(): Promise { + if (this.worker) { + await this.worker.terminate().catch(); // best effort + this.worker = undefined; } - const { count, start } = pageRange; + } - return start + count; + public async generate(): Promise { + if (this.worker) throw new Error('PDF generation already in progress!'); + + try { + return await new Promise((resolve, reject) => { + const { port1: myPort, port2: theirPort } = new MessageChannel(); + this.worker = this.createWorker(theirPort); + this.worker.on('error', (workerError: NodeJS.ErrnoException) => { + if (workerError.code === 'ERR_WORKER_OUT_OF_MEMORY') { + reject(new PdfWorkerOutOfMemoryError(workerError.message)); + } else { + reject(workerError); + } + }); + this.worker.on('exit', () => {}); // do nothing on errors + + // Send the initial request + const generatePdfRequest: GeneratePdfRequest = { + data: this.getGeneratePdfRequestData(), + }; + myPort.postMessage(generatePdfRequest); + + // We expect one message from the worker generating the PDF buffer. + myPort.on('message', ({ error, data }: GeneratePdfResponse) => { + if (error) { + reject(new Error(`PDF worker returned the following error: ${error}`)); + return; + } + if (!data) { + reject(new Error(`Worker did not generate a PDF!`)); + return; + } + this.pageCount = data.metrics.pages; + resolve(data.buffer); + }); + }); + } finally { + await this.cleanupWorker(); + } + } + + getPageCount(): number { + return this.pageCount; } } diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker_errors.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker_errors.ts new file mode 100644 index 00000000000000..d55921d791aded --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/pdfmaker_errors.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class PdfWorkerOutOfMemoryError extends Error { + constructor(message: string) { + super(message); + this.name = 'PdfWorkerOutOfMemoryError'; + } +} diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/types.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/types.ts new file mode 100644 index 00000000000000..e622cd49ad13e0 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/types.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 { Ensure, SerializableRecord } from '@kbn/utility-types'; + +export type TemplateLayout = Ensure< + { + orientation: 'landscape' | 'portrait' | undefined; + useReportingBranding: boolean; + hasHeader: boolean; + hasFooter: boolean; + pageSize: + | string + | { + width: number; + height: number | 'auto'; + }; + }, + SerializableRecord +>; diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/worker.js b/x-pack/plugins/reporting/server/export_types/common/pdf/worker.js new file mode 100644 index 00000000000000..d3dfa3e9accf82 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/worker.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +require('../../../../../../../src/setup_node_env'); +require('./worker.ts'); diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/worker.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/worker.ts new file mode 100644 index 00000000000000..983cebca7d6ae4 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/worker.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Ensure, SerializableRecord } from '@kbn/utility-types'; + +import { isMainThread, MessagePort, workerData } from 'worker_threads'; +import path from 'path'; + +import { getTemplate } from './get_template'; +import type { TemplateLayout } from './types'; +import { assetPath } from './constants'; +import { _, Printer } from './worker_dependencies'; + +export interface WorkerData { + port: MessagePort; +} + +export type GenerateReportRequestData = Ensure< + { + layout: TemplateLayout; + title: string; + content: SerializableRecord[]; + + logo?: string; + }, + SerializableRecord +>; + +export interface GeneratePdfRequest { + data: GenerateReportRequestData; +} + +export type GeneratePdfResponse = SuccessResponse | ErrorResponse; + +export interface SuccessResponse { + error?: undefined; + data: { + buffer: Uint8Array; + metrics: { + pages: number; + }; + }; +} + +export interface ErrorResponse { + error: string; + data: null; +} + +if (!isMainThread) { + const { port } = workerData as WorkerData; + port.on('message', execute); +} + +const getPageCount = (pdfDoc: PDFKit.PDFDocument): number => { + const pageRange = pdfDoc.bufferedPageRange(); + if (!pageRange) { + return 0; + } + const { count, start } = pageRange; + + return start + count; +}; + +async function execute({ data: { layout, logo, title, content } }: GeneratePdfRequest) { + const { port } = workerData as WorkerData; + try { + const tableBorderWidth = 1; + + const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); + + const fonts = { + Roboto: { + normal: fontPath('roboto/Roboto-Regular.ttf'), + bold: fontPath('roboto/Roboto-Medium.ttf'), + italics: fontPath('roboto/Roboto-Italic.ttf'), + bolditalics: fontPath('roboto/Roboto-Italic.ttf'), + }, + 'noto-cjk': { + // Roboto does not support CJK characters, so we'll fall back on this font if we detect them. + normal: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bold: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + italics: fontPath('noto/NotoSansCJKtc-Regular.ttf'), + bolditalics: fontPath('noto/NotoSansCJKtc-Medium.ttf'), + }, + }; + + const printer = new Printer(fonts); + + const docDefinition = _.assign(getTemplate(layout, logo, title, tableBorderWidth, assetPath), { + content, + }); + + const pdfDoc = printer.createPdfKitDocument(docDefinition, { + tableLayouts: { + noBorder: { + // format is function (i, node) { ... }; + hLineWidth: () => 0, + vLineWidth: () => 0, + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, + }, + }, + }); + + if (!pdfDoc) { + throw new Error('Document stream has not been generated'); + } + + const buffer = await new Promise((resolve, reject) => { + const buffers: Buffer[] = []; + pdfDoc.on('error', reject); + pdfDoc.on('data', (data: Buffer) => { + buffers.push(data); + }); + pdfDoc.on('end', () => { + resolve(Buffer.concat(buffers)); + }); + pdfDoc.end(); + }); + + const successResponse: SuccessResponse = { + data: { + buffer, + metrics: { + pages: getPageCount(pdfDoc), + }, + }, + }; + port.postMessage(successResponse, [buffer.buffer /* Transfer buffer instead of copying */]); + } catch (error) { + const errorResponse: ErrorResponse = { error: error.message, data: null }; + port.postMessage(errorResponse); + } finally { + process.nextTick(() => { + process.exit(0); + }); + } +} diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/worker_dependencies.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/worker_dependencies.ts new file mode 100644 index 00000000000000..58e2248945a48f --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/worker_dependencies.ts @@ -0,0 +1,11 @@ +/* + * 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 _ from 'lodash'; +import Printer from 'pdfmake'; + +export { _, Printer }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 149f4fc3aee520..459887ebb81187 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -13,7 +13,7 @@ import type { PdfMetrics } from '../../../../common/types'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { ScreenshotOptions } from '../../../types'; -import { PdfMaker } from '../../common/pdf'; +import { PdfMaker, PdfWorkerOutOfMemoryError } from '../../common/pdf'; import { getTracker } from './tracker'; const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => { @@ -27,7 +27,7 @@ const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => { }; interface PdfResult { - buffer: Buffer | null; + buffer: Uint8Array | null; metrics?: PdfMetrics; warnings: string[]; } @@ -72,44 +72,49 @@ export function generatePdfObservable( }); }); - let buffer: Buffer | null = null; + const warnings = results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, []); + + let buffer: Uint8Array | null = null; try { tracker.startCompile(); logger.info(`Compiling PDF using "${layout.id}" layout...`); - pdfOutput.generate(); + buffer = await pdfOutput.generate(); tracker.endCompile(); - tracker.startGetBuffer(); logger.debug(`Generating PDF Buffer...`); - buffer = await pdfOutput.getBuffer(); const byteLength = buffer?.byteLength ?? 0; logger.debug(`PDF buffer byte length: ${byteLength}`); tracker.setByteLength(byteLength); - - tracker.endGetBuffer(); } catch (err) { logger.error(`Could not generate the PDF buffer!`); logger.error(err); + if (err instanceof PdfWorkerOutOfMemoryError) { + warnings.push( + 'Failed to generate PDF due to low memory. Please consider generating a smaller PDF.' + ); + } else { + warnings.push(`Failed to generate PDF due to the following error: ${err.message}`); + } } tracker.end(); return { buffer, + warnings, metrics: { ...metrics, pages: pdfOutput.getPageCount(), }, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), }; }) ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts index d1cf2b96817d24..62094be45ef351 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts @@ -20,8 +20,6 @@ interface PdfTracker { endAddImage: () => void; startCompile: () => void; endCompile: () => void; - startGetBuffer: () => void; - endGetBuffer: () => void; end: () => void; } @@ -39,7 +37,6 @@ export function getTracker(): PdfTracker { let apmSetup: ApmSpan | null = null; let apmAddImage: ApmSpan | null = null; let apmCompilePdf: ApmSpan | null = null; - let apmGetBuffer: ApmSpan | null = null; return { startScreenshots() { @@ -66,12 +63,6 @@ export function getTracker(): PdfTracker { endCompile() { if (apmCompilePdf) apmCompilePdf.end(); }, - startGetBuffer() { - apmGetBuffer = apmTrans?.startSpan('get-buffer', SPANTYPE_OUTPUT) || null; - }, - endGetBuffer() { - if (apmGetBuffer) apmGetBuffer.end(); - }, setByteLength(byteLength: number) { apmTrans?.setLabel('byte-length', byteLength, false); }, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index 08e73371f74b7a..b0ac1a59010a69 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -9,11 +9,12 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; +import { ScreenshotResult } from '../../../../../screenshotting/server'; import { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; import { LevelLogger } from '../../../lib'; -import { ScreenshotResult } from '../../../../../screenshotting/server'; import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; +import { PdfWorkerOutOfMemoryError } from '../../common/pdf'; import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; import type { TaskPayloadPDFV2 } from '../types'; import { getTracker } from './tracker'; @@ -29,7 +30,7 @@ const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => { }; interface PdfResult { - buffer: Buffer | null; + buffer: Uint8Array | null; metrics?: PdfMetrics; warnings: string[]; } @@ -84,44 +85,47 @@ export function generatePdfObservable( }); }); - let buffer: Buffer | null = null; + const warnings = results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, []); + + let buffer: Uint8Array | null = null; try { tracker.startCompile(); logger.info(`Compiling PDF using "${layout.id}" layout...`); - pdfOutput.generate(); + buffer = await pdfOutput.generate(); tracker.endCompile(); - tracker.startGetBuffer(); - logger.debug(`Generating PDF Buffer...`); - buffer = await pdfOutput.getBuffer(); - const byteLength = buffer?.byteLength ?? 0; logger.debug(`PDF buffer byte length: ${byteLength}`); tracker.setByteLength(byteLength); - tracker.endGetBuffer(); + tracker.end(); } catch (err) { logger.error(`Could not generate the PDF buffer!`); logger.error(err); + if (err instanceof PdfWorkerOutOfMemoryError) { + warnings.push( + 'Failed to generate PDF due to low memory. Please consider generating a smaller PDF.' + ); + } else { + warnings.push(`Failed to generate PDF due to the following error: ${err.message}`); + } } - tracker.end(); - return { buffer, + warnings, metrics: { ...metrics, pages: pdfOutput.getPageCount(), }, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), }; }) ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts index d1cf2b96817d24..62094be45ef351 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts @@ -20,8 +20,6 @@ interface PdfTracker { endAddImage: () => void; startCompile: () => void; endCompile: () => void; - startGetBuffer: () => void; - endGetBuffer: () => void; end: () => void; } @@ -39,7 +37,6 @@ export function getTracker(): PdfTracker { let apmSetup: ApmSpan | null = null; let apmAddImage: ApmSpan | null = null; let apmCompilePdf: ApmSpan | null = null; - let apmGetBuffer: ApmSpan | null = null; return { startScreenshots() { @@ -66,12 +63,6 @@ export function getTracker(): PdfTracker { endCompile() { if (apmCompilePdf) apmCompilePdf.end(); }, - startGetBuffer() { - apmGetBuffer = apmTrans?.startSpan('get-buffer', SPANTYPE_OUTPUT) || null; - }, - endGetBuffer() { - if (apmGetBuffer) apmGetBuffer.end(); - }, setByteLength(byteLength: number) { apmTrans?.setLabel('byte-length', byteLength, false); }, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index baa60664dea577..3593030913ba74 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -72,6 +72,7 @@ function createRule(shouldWriteAlerts: boolean = true) { scheduleActions, } as any; }, + done: () => ({ getRecoveredAlerts: () => [] }), }; return { diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts index 79a2314af9397a..1b9c63dd2dbcb3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts @@ -26,7 +26,7 @@ describe('Create DataView runtime field', () => { cleanKibana(); }); - it('adds field to alert table', () => { + it.skip('adds field to alert table', () => { const fieldName = 'field.name.alert.page'; loginAndWaitForPage(ALERTS_URL); createCustomRuleActivated(getNewRule()); diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts index 62ba50a494df55..05b9cb567fafdc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts @@ -139,7 +139,7 @@ describe('Timeline scope', () => { loginAndWaitForPage(TIMELINES_URL); }); - it('correctly loads SIEM data view before and after signals index exists', () => { + it.skip('correctly loads SIEM data view before and after signals index exists', () => { openTimelineUsingToggle(); openSourcerer('timeline'); isDataViewSelection(siemDataViewTitle); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts index 0887c4774f9a8a..32ce0bebda2251 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts @@ -26,7 +26,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Marking alerts as acknowledged', () => { +describe.skip('Marking alerts as acknowledged', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(ALERTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 16beb418d0d13b..2d5a6766466880 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -26,7 +26,7 @@ import { getUnmappedRule } from '../../objects/rule'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Alert details with unmapped fields', () => { +describe.skip('Alert details with unmapped fields', () => { beforeEach(() => { cleanKibana(); esArchiverLoad('unmapped_fields'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts index ce232f7b841571..436ef0975ef020 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts @@ -23,7 +23,7 @@ const loadDetectionsPage = (role: ROLES) => { waitForAlertsToPopulate(); }; -describe('Alerts timeline', () => { +describe.skip('Alerts timeline', () => { before(() => { // First we login as a privileged user to create alerts. cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts index bdd83d93fa25db..288d16dc22fb61 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts @@ -18,7 +18,7 @@ import { ALERTS_URL, DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigatio const EXPECTED_NUMBER_OF_ALERTS = 16; -describe('Alerts generated by building block rules', () => { +describe.skip('Alerts generated by building block rules', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index efa7f31455b1f2..af2772b98a7908 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -31,7 +31,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Closing alerts', () => { +describe.skip('Closing alerts', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(ALERTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index 0e4dbc9a95f9c9..c5e015b6382c29 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -33,7 +33,7 @@ import { openJsonView, openThreatIndicatorDetails } from '../../tasks/alerts_det import { ALERTS_URL } from '../../urls/navigation'; import { addsFieldsToTimeline } from '../../tasks/rule_details'; -describe('CTI Enrichment', () => { +describe.skip('CTI Enrichment', () => { before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts index 033a559ffff98c..e8873de412f4c2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts @@ -17,7 +17,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Alerts timeline', () => { +describe.skip('Alerts timeline', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(ALERTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts index 20a2f6ebed3e2f..ece7dbe5596720 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts @@ -29,7 +29,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -describe('Opening alerts', () => { +describe.skip('Opening alerts', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(ALERTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 530ec4934b447a..b98f626c6356c6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -105,7 +105,7 @@ import { activatesRule, getDetails } from '../../tasks/rule_details'; import { RULE_CREATION, DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -describe('Custom detection rules creation', () => { +describe.skip('Custom detection rules creation', () => { const expectedUrls = getNewRule().referenceUrls.join(''); const expectedFalsePositives = getNewRule().falsePositivesExamples.join(''); const expectedTags = getNewRule().tags.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index c3797cd4b178cd..8384c879d81108 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -63,7 +63,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { RULE_CREATION } from '../../urls/navigation'; -describe('Detection rules, EQL', () => { +describe.skip('Detection rules, EQL', () => { const expectedUrls = getEqlRule().referenceUrls.join(''); const expectedFalsePositives = getEqlRule().falsePositivesExamples.join(''); const expectedTags = getEqlRule().tags.join(''); @@ -159,7 +159,7 @@ describe('Detection rules, EQL', () => { }); }); -describe('Detection rules, sequence EQL', () => { +describe.skip('Detection rules, sequence EQL', () => { const expectedNumberOfRules = 1; const expectedNumberOfSequenceAlerts = '1 alert'; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index da8b089ee186d9..d34d9bd4fc171e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -114,7 +114,7 @@ import { goBackToAllRulesTable, getDetails } from '../../tasks/rule_details'; import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d/d"'; -describe('indicator match', () => { +describe.skip('indicator match', () => { describe('Detection rules, Indicator Match', () => { const expectedUrls = getNewThreatIndicatorRule().referenceUrls.join(''); const expectedFalsePositives = getNewThreatIndicatorRule().falsePositivesExamples.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts index e0596d52809e02..bf8d753a8161c6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts @@ -57,7 +57,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { RULE_CREATION } from '../../urls/navigation'; -describe('Detection rules, machine learning', () => { +describe.skip('Detection rules, machine learning', () => { const expectedUrls = getMachineLearningRule().referenceUrls.join(''); const expectedFalsePositives = getMachineLearningRule().falsePositivesExamples.join(''); const expectedTags = getMachineLearningRule().tags.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 6d078b3da24c01..694036d8a1678d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -73,7 +73,7 @@ import { getDetails } from '../../tasks/rule_details'; import { RULE_CREATION } from '../../urls/navigation'; -describe('Detection rules, override', () => { +describe.skip('Detection rules, override', () => { const expectedUrls = getNewOverrideRule().referenceUrls.join(''); const expectedFalsePositives = getNewOverrideRule().falsePositivesExamples.join(''); const expectedTags = getNewOverrideRule().tags.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index ec84359e637123..921128ce3303dd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -77,7 +77,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { RULE_CREATION } from '../../urls/navigation'; -describe('Detection rules, threshold', () => { +describe.skip('Detection rules, threshold', () => { let rule = getNewThresholdRule(); const expectedUrls = getNewThresholdRule().referenceUrls.join(''); const expectedFalsePositives = getNewThresholdRule().falsePositivesExamples.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts index 2e5994e73ac523..9887eb1e8612ba 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts @@ -29,7 +29,7 @@ import { import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; import { cleanKibana, reload } from '../../tasks/common'; -describe('From alert', () => { +describe.skip('From alert', () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; beforeEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts index 1262bea01708df..d9661324aee6db 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts @@ -30,7 +30,7 @@ import { refreshPage } from '../../tasks/security_header'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; import { cleanKibana, reload } from '../../tasks/common'; -describe('From rule', () => { +describe.skip('From rule', () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1'; beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/users/user_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/users/user_details.spec.ts index d718b499f199d8..a30b651bfba39e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/users/user_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/users/user_details.spec.ts @@ -20,7 +20,7 @@ import { } from '../../tasks/alerts'; import { USER_COLUMN } from '../../screens/alerts'; -describe('user details flyout', () => { +describe.skip('user details flyout', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(ALERTS_URL); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 775817dcb8a0c0..11396864d802d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -145,8 +145,15 @@ export const previewRulesRoute = async ( id: string ) => Pick< Alert, - 'getState' | 'replaceState' | 'scheduleActions' | 'scheduleActionsWithSubGroup' + | 'getState' + | 'replaceState' + | 'scheduleActions' + | 'scheduleActionsWithSubGroup' + | 'setContext' + | 'getContext' + | 'hasContext' >; + done: () => { getRecoveredAlerts: () => [] }; } ) => { let statePreview = runState as TState; @@ -228,7 +235,7 @@ export const previewRulesRoute = async ( queryAlertType.name, previewRuleParams, () => true, - { create: alertInstanceFactoryStub } + { create: alertInstanceFactoryStub, done: () => ({ getRecoveredAlerts: () => [] }) } ); break; case 'threshold': @@ -241,7 +248,7 @@ export const previewRulesRoute = async ( thresholdAlertType.name, previewRuleParams, () => true, - { create: alertInstanceFactoryStub } + { create: alertInstanceFactoryStub, done: () => ({ getRecoveredAlerts: () => [] }) } ); break; case 'threat_match': @@ -254,7 +261,7 @@ export const previewRulesRoute = async ( threatMatchAlertType.name, previewRuleParams, () => true, - { create: alertInstanceFactoryStub } + { create: alertInstanceFactoryStub, done: () => ({ getRecoveredAlerts: () => [] }) } ); break; case 'eql': @@ -265,7 +272,7 @@ export const previewRulesRoute = async ( eqlAlertType.name, previewRuleParams, () => true, - { create: alertInstanceFactoryStub } + { create: alertInstanceFactoryStub, done: () => ({ getRecoveredAlerts: () => [] }) } ); break; case 'machine_learning': @@ -276,7 +283,7 @@ export const previewRulesRoute = async ( mlAlertType.name, previewRuleParams, () => true, - { create: alertInstanceFactoryStub } + { create: alertInstanceFactoryStub, done: () => ({ getRecoveredAlerts: () => [] }) } ); break; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 3d96e3bb779079..3d390cac6b91fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -76,7 +76,10 @@ export const createRuleTypeMocks = ( search: elasticsearchServiceMock.createScopedClusterClient(), savedObjectsClient: mockSavedObjectsClient, scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { create: jest.fn(() => ({ scheduleActions })) }, + alertFactory: { + create: jest.fn(() => ({ scheduleActions })), + done: jest.fn().mockResolvedValue({}), + }, findAlerts: jest.fn(), // TODO: does this stay? alertWithPersistence: jest.fn(), logger: loggerMock, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts index 7cc709bbe89942..88d6114387aa35 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/alert_instance_factory_stub.ts @@ -27,13 +27,13 @@ export const alertInstanceFactoryStub = < return {} as unknown as TInstanceState; }, replaceState(state: TInstanceState) { - return new Alert({ + return new Alert('', { state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); }, scheduleActions(actionGroup: TActionGroupIds, alertcontext: TInstanceContext) { - return new Alert({ + return new Alert('', { state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); @@ -43,9 +43,21 @@ export const alertInstanceFactoryStub = < subgroup: string, alertcontext: TInstanceContext ) { - return new Alert({ + return new Alert('', { state: {} as TInstanceState, meta: { lastScheduledActions: { group: 'default', date: new Date() } }, }); }, + setContext(alertContext: TInstanceContext) { + return new Alert('', { + state: {} as TInstanceState, + meta: { lastScheduledActions: { group: 'default', date: new Date() } }, + }); + }, + getContext() { + return {} as unknown as TInstanceContext; + }, + hasContext() { + return false; + }, }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index a625d9c193cd65..402d6fca7a1a9d 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -13,6 +13,8 @@ import { XJsonMode } from '@kbn/ace'; import 'brace/theme/github'; import { + EuiFlexGroup, + EuiFlexItem, EuiButtonEmpty, EuiSpacer, EuiFormRow, @@ -20,6 +22,7 @@ import { EuiText, EuiTitle, EuiLink, + EuiIconTip, } from '@elastic/eui'; import { DocLinksStart, HttpSetup } from 'kibana/public'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -347,14 +350,31 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< )} - -
- + + +
+ +
+
+
+ + -
-
+ + ); return alertInstance; }, + done: () => ({ getRecoveredAlerts: () => [] }), }); describe('geo_containment', () => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts index 9a5e0de9d53bf3..4d8c1dc3d9b9f2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts @@ -96,6 +96,36 @@ describe('ActionContext', () => { - Value: 4 - Conditions Met: count between 4 and 5 over 5m +- Timestamp: 2020-01-01T00:00:00.000Z` + ); + }); + + it('generates expected properties if value is string', async () => { + const params = ParamsSchema.validate({ + index: '[index]', + timeField: '[timeField]', + aggType: 'count', + groupBy: 'top', + termField: 'x', + termSize: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: 'between', + threshold: [4, 5], + }); + const base: BaseActionContext = { + date: '2020-01-01T00:00:00.000Z', + group: '[group]', + value: 'unknown', + conditions: 'count between 4 and 5', + }; + const context = addMessages({ name: '[alert-name]' }, base, params); + expect(context.title).toMatchInlineSnapshot(`"alert [alert-name] group [group] met threshold"`); + expect(context.message).toEqual( + `alert '[alert-name]' is active for group '[group]': + +- Value: unknown +- Conditions Met: count between 4 and 5 over 5m - Timestamp: 2020-01-01T00:00:00.000Z` ); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts index 69ca1c2700ebe9..02450da5bbdf7f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts @@ -11,7 +11,7 @@ import { AlertExecutorOptions, AlertInstanceContext } from '../../../../alerting // alert type context provided to actions -type AlertInfo = Pick; +type RuleInfo = Pick; export interface ActionContext extends BaseActionContext { // a short pre-constructed message which may be used in an action field @@ -27,43 +27,72 @@ export interface BaseActionContext extends AlertInstanceContext { // the date the alert was run as an ISO date date: string; // the value that met the threshold - value: number; + value: number | string; // threshold conditions conditions: string; } -export function addMessages( - alertInfo: AlertInfo, - baseContext: BaseActionContext, - params: Params -): ActionContext { - const title = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle', { +const DEFAULT_TITLE = (name: string, group: string) => + i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle', { defaultMessage: 'alert {name} group {group} met threshold', + values: { name, group }, + }); + +const RECOVERY_TITLE = (name: string, group: string) => + i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeRecoveryContextSubjectTitle', { + defaultMessage: 'alert {name} group {group} recovered', + values: { name, group }, + }); + +const DEFAULT_MESSAGE = (name: string, context: BaseActionContext, window: string) => + i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription', { + defaultMessage: `alert '{name}' is active for group '{group}': + +- Value: {value} +- Conditions Met: {conditions} over {window} +- Timestamp: {date}`, values: { - name: alertInfo.name, - group: baseContext.group, + name, + group: context.group, + value: context.value, + conditions: context.conditions, + window, + date: context.date, }, }); - const window = `${params.timeWindowSize}${params.timeWindowUnit}`; - const message = i18n.translate( - 'xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription', - { - defaultMessage: `alert '{name}' is active for group '{group}': +const RECOVERY_MESSAGE = (name: string, context: BaseActionContext, window: string) => + i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeRecoveryContextMessageDescription', { + defaultMessage: `alert '{name}' is recovered for group '{group}': - Value: {value} - Conditions Met: {conditions} over {window} - Timestamp: {date}`, - values: { - name: alertInfo.name, - group: baseContext.group, - value: baseContext.value, - conditions: baseContext.conditions, - window, - date: baseContext.date, - }, - } - ); + values: { + name, + group: context.group, + value: context.value, + conditions: context.conditions, + window, + date: context.date, + }, + }); + +export function addMessages( + ruleInfo: RuleInfo, + baseContext: BaseActionContext, + params: Params, + isRecoveryMessage?: boolean +): ActionContext { + const title = isRecoveryMessage + ? RECOVERY_TITLE(ruleInfo.name, baseContext.group) + : DEFAULT_TITLE(ruleInfo.name, baseContext.group); + + const window = `${params.timeWindowSize}${params.timeWindowUnit}`; + + const message = isRecoveryMessage + ? RECOVERY_MESSAGE(ruleInfo.name, baseContext, window) + : DEFAULT_MESSAGE(ruleInfo.name, baseContext, window); return { ...baseContext, title, message }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index 0eb2810626ac36..7725721ed8efa9 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -128,12 +128,13 @@ export function getAlertType( isExportable: true, executor, producer: STACK_ALERTS_FEATURE_ID, + doesSetRecoveryContext: true, }; async function executor( options: AlertExecutorOptions ) { - const { alertId, name, services, params } = options; + const { alertId: ruleId, name, services, params } = options; const { alertFactory, search } = services; const compareFn = ComparatorFns.get(params.thresholdComparator); @@ -173,19 +174,22 @@ export function getAlertType( abortableEsClient, query: queryParams, }); - logger.debug(`alert ${ID}:${alertId} "${name}" query result: ${JSON.stringify(result)}`); + logger.debug(`rule ${ID}:${ruleId} "${name}" query result: ${JSON.stringify(result)}`); + + const unmetGroupValues: Record = {}; + const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`; const groupResults = result.results || []; // console.log(`index_threshold: response: ${JSON.stringify(groupResults, null, 4)}`); for (const groupResult of groupResults) { - const instanceId = groupResult.group; + const alertId = groupResult.group; const metric = groupResult.metrics && groupResult.metrics.length > 0 ? groupResult.metrics[0] : null; const value = metric && metric.length === 2 ? metric[1] : null; if (value === null || value === undefined) { logger.debug( - `alert ${ID}:${alertId} "${name}": no metrics found for group ${instanceId}} from groupResult ${JSON.stringify( + `rule ${ID}:${ruleId} "${name}": no metrics found for group ${alertId}} from groupResult ${JSON.stringify( groupResult )}` ); @@ -194,23 +198,41 @@ export function getAlertType( const met = compareFn(value, params.threshold); - if (!met) continue; + if (!met) { + unmetGroupValues[alertId] = value; + continue; + } - const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`; const humanFn = `${agg} is ${getHumanReadableComparator( params.thresholdComparator )} ${params.threshold.join(' and ')}`; const baseContext: BaseActionContext = { date, - group: instanceId, + group: alertId, value, conditions: humanFn, }; const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertFactory.create(instanceId); - alertInstance.scheduleActions(ActionGroupId, actionContext); + const alert = alertFactory.create(alertId); + alert.scheduleActions(ActionGroupId, actionContext); logger.debug(`scheduled actionGroup: ${JSON.stringify(actionContext)}`); } + + const { getRecoveredAlerts } = services.alertFactory.done(); + for (const recoveredAlert of getRecoveredAlerts()) { + const alertId = recoveredAlert.getId(); + logger.debug(`setting context for recovered alert ${alertId}`); + const baseContext: BaseActionContext = { + date, + value: unmetGroupValues[alertId] ?? 'unknown', + group: alertId, + conditions: `${agg} is NOT ${getHumanReadableComparator( + params.thresholdComparator + )} ${params.threshold.join(' and ')}`, + }; + const recoveryContext = addMessages(options, baseContext, params, true); + recoveredAlert.setContext(recoveryContext); + } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4810fc7ba89a06..c62e8cfc237dee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20444,7 +20444,6 @@ "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました。{encryptionKey}が設定されていることを確認してこのレポートを再生成してください。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "ジョブヘッダーがありません", "xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues": "CSVには、値がエスケープされた式が含まれる場合があります", - "xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage": "ドキュメントストリームが生成されていません。", "xpack.reporting.exportTypes.printablePdf.logoDescription": "Elastic 提供", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "{pageCount} ページ中 {currentPage} ページ目", "xpack.reporting.jobCreatedBy.unknownUserPlaceholderText": "不明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 72cd8e05ccfb0f..a26abde5f10a69 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20500,7 +20500,6 @@ "xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}", "xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "作业标头缺失", "xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues": "CSV 可能包含值已转义的公式", - "xpack.reporting.exportTypes.printablePdf.documentStreamIsNotgeneratedErrorMessage": "尚未生成文档流", "xpack.reporting.exportTypes.printablePdf.logoDescription": "由 Elastic 提供支持", "xpack.reporting.exportTypes.printablePdf.pagingDescription": "第 {currentPage} 页,共 {pageCount} 页", "xpack.reporting.jobCreatedBy.unknownUserPlaceholderText": "未知", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 30e594f35d1f89..c2ab2936e0cc76 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -191,17 +191,30 @@ describe('transformActionVariables', () => { ]); }); - test('should return only the required action variables when omitOptionalMessageVariables is provided', () => { + test(`should return only the required action variables when omitMessageVariables is "all"`, () => { const alertType = getAlertType({ context: mockContextVariables(), state: mockStateVariables(), params: mockParamsVariables(), }); - expect(transformActionVariables(alertType.actionVariables, true)).toEqual([ + expect(transformActionVariables(alertType.actionVariables, 'all')).toEqual([ ...expectedTransformResult, ...expectedParamsTransformResult(), ]); }); + + test(`should return required and context action variables when omitMessageVariables is "keepContext"`, () => { + const alertType = getAlertType({ + context: mockContextVariables(), + state: mockStateVariables(), + params: mockParamsVariables(), + }); + expect(transformActionVariables(alertType.actionVariables, 'keepContext')).toEqual([ + ...expectedTransformResult, + ...expectedContextTransformResult(), + ...expectedParamsTransformResult(), + ]); + }); }); function getAlertType(actionVariables: ActionVariables): RuleType { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts index 2cf1df85a3447b..6aff0781a56c28 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts @@ -7,16 +7,20 @@ import { i18n } from '@kbn/i18n'; import { pick } from 'lodash'; -import { ActionVariables, REQUIRED_ACTION_VARIABLES } from '../../types'; +import { ActionVariables, REQUIRED_ACTION_VARIABLES, CONTEXT_ACTION_VARIABLES } from '../../types'; import { ActionVariable } from '../../../../alerting/common'; +export type OmitMessageVariablesType = 'all' | 'keepContext'; + // return a "flattened" list of action variables for an alertType export function transformActionVariables( actionVariables: ActionVariables, - omitOptionalMessageVariables?: boolean + omitMessageVariables?: OmitMessageVariablesType ): ActionVariable[] { - const filteredActionVariables: ActionVariables = omitOptionalMessageVariables - ? pick(actionVariables, ...REQUIRED_ACTION_VARIABLES) + const filteredActionVariables: ActionVariables = omitMessageVariables + ? omitMessageVariables === 'all' + ? pick(actionVariables, REQUIRED_ACTION_VARIABLES) + : pick(actionVariables, [...REQUIRED_ACTION_VARIABLES, ...CONTEXT_ACTION_VARIABLES]) : actionVariables; const alwaysProvidedVars = getAlwaysProvidedActionVariables(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts index 99478f250f6a29..63207dd35dfaca 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts @@ -22,6 +22,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ action_variables: actionVariables, authorized_consumers: authorizedConsumers, rule_task_timeout: ruleTaskTimeout, + does_set_recovery_context: doesSetRecoveryContext, ...rest }: AsApiContract) => ({ enabledInLicense, @@ -32,6 +33,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ actionVariables, authorizedConsumers, ruleTaskTimeout, + doesSetRecoveryContext, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 836e63fa7a68ae..f25827fb4ba993 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -40,9 +40,10 @@ import { useKibana } from '../../../common/lib/kibana'; import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params'; import { ConnectorAddModal } from '.'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +import { OmitMessageVariablesType } from '../../lib/action_variables'; export interface ActionGroupWithMessageVariables extends ActionGroup { - omitOptionalMessageVariables?: boolean; + omitMessageVariables?: OmitMessageVariablesType; defaultActionMessage?: string; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 4a4230c233dfae..4a27c4c1e6fefc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -346,7 +346,7 @@ function getAvailableActionVariables( ) { const transformedActionVariables: ActionVariable[] = transformActionVariables( actionVariables, - actionGroup?.omitOptionalMessageVariables + actionGroup?.omitMessageVariables ); // partition deprecated items so they show up last diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 06542cbb3a1a4c..fa226c4a74cdd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -556,7 +556,9 @@ export const AlertForm = ({ actionGroup.id === selectedAlertType.recoveryActionGroup.id ? { ...actionGroup, - omitOptionalMessageVariables: true, + omitMessageVariables: selectedAlertType.doesSetRecoveryContext + ? 'keepContext' + : 'all', defaultActionMessage: recoveredActionGroupMessage, } : { ...actionGroup, defaultActionMessage: alertTypeModel?.defaultActionMessage } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 72fd48d3557745..718a637518cbec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -205,7 +205,8 @@ type AsActionVariables = { [Req in Keys]: ActionVariable[]; }; export const REQUIRED_ACTION_VARIABLES = ['params'] as const; -export const OPTIONAL_ACTION_VARIABLES = ['state', 'context'] as const; +export const CONTEXT_ACTION_VARIABLES = ['context'] as const; +export const OPTIONAL_ACTION_VARIABLES = [...CONTEXT_ACTION_VARIABLES, 'state'] as const; export type ActionVariables = AsActionVariables & Partial>; @@ -224,6 +225,7 @@ export interface RuleType< | 'ruleTaskTimeout' | 'defaultScheduleInterval' | 'minimumScheduleInterval' + | 'doesSetRecoveryContext' > { actionVariables: ActionVariables; authorizedConsumers: Record; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts index b070219410fd97..0c527ac1449f84 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts @@ -21,6 +21,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { { id: 'recovered', name: 'Recovered' }, ], default_action_group_id: 'default', + does_set_recovery_context: false, id: 'test.noop', name: 'Test: Noop', action_variables: { @@ -49,6 +50,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { name: 'Restricted Recovery', }, default_action_group_id: 'default', + does_set_recovery_context: false, id: 'test.restricted-noop', name: 'Test: Restricted Noop', action_variables: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index 8c796c1a39d679..ba2a2cb8fdf470 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -17,27 +17,27 @@ import { } from '../../../../../common/lib'; import { createEsDocuments } from './create_test_data'; -const ALERT_TYPE_ID = '.index-threshold'; -const ACTION_TYPE_ID = '.index'; +const RULE_TYPE_ID = '.index-threshold'; +const CONNECTOR_TYPE_ID = '.index'; const ES_TEST_INDEX_SOURCE = 'builtin-alert:index-threshold'; const ES_TEST_INDEX_REFERENCE = '-na-'; const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`; -const ALERT_INTERVALS_TO_WRITE = 5; -const ALERT_INTERVAL_SECONDS = 3; -const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; +const RULE_INTERVALS_TO_WRITE = 5; +const RULE_INTERVAL_SECONDS = 3; +const RULE_INTERVAL_MILLIS = RULE_INTERVAL_SECONDS * 1000; // eslint-disable-next-line import/no-default-export -export default function alertTests({ getService }: FtrProviderContext) { +export default function ruleTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); - describe('alert', async () => { + describe('rule', async () => { let endDate: string; - let actionId: string; + let connectorId: string; const objectRemover = new ObjectRemover(supertest); beforeEach(async () => { @@ -47,10 +47,10 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); await esTestIndexToolOutput.setup(); - actionId = await createAction(supertest, objectRemover); + connectorId = await createConnector(supertest, objectRemover); // write documents in the future, figure out the end date - const endDateMillis = Date.now() + (ALERT_INTERVALS_TO_WRITE - 1) * ALERT_INTERVAL_MILLIS; + const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS; endDate = new Date(endDateMillis).toISOString(); // write documents from now to the future end date in 3 groups @@ -67,7 +67,7 @@ export default function alertTests({ getService }: FtrProviderContext) { // never fire; the tests ensure the ones that should fire, do fire, and // those that shouldn't fire, do not fire. it('runs correctly: count all < >', async () => { - await createAlert({ + await createRule({ name: 'never fire', aggType: 'count', groupBy: 'all', @@ -75,7 +75,7 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: [0], }); - await createAlert({ + await createRule({ name: 'always fire', aggType: 'count', groupBy: 'all', @@ -104,7 +104,7 @@ export default function alertTests({ getService }: FtrProviderContext) { // create some more documents in the first group createEsDocumentsInGroups(1); - await createAlert({ + await createRule({ name: 'never fire', aggType: 'count', groupBy: 'top', @@ -114,7 +114,7 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: [-1], }); - await createAlert({ + await createRule({ name: 'always fire', aggType: 'count', groupBy: 'top', @@ -148,7 +148,7 @@ export default function alertTests({ getService }: FtrProviderContext) { // create some more documents in the first group createEsDocumentsInGroups(1); - await createAlert({ + await createRule({ name: 'never fire', aggType: 'sum', aggField: 'testedValue', @@ -157,7 +157,7 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: [-2, -1], }); - await createAlert({ + await createRule({ name: 'always fire', aggType: 'sum', aggField: 'testedValue', @@ -183,7 +183,7 @@ export default function alertTests({ getService }: FtrProviderContext) { createEsDocumentsInGroups(1); // this never fires because of bad fields error - await createAlert({ + await createRule({ name: 'never fire', timeField: 'source', // bad field for time aggType: 'avg', @@ -193,7 +193,7 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: [0], }); - await createAlert({ + await createRule({ name: 'always fire', aggType: 'avg', aggField: 'testedValue', @@ -218,7 +218,7 @@ export default function alertTests({ getService }: FtrProviderContext) { // create some more documents in the first group createEsDocumentsInGroups(1); - await createAlert({ + await createRule({ name: 'never fire', aggType: 'max', aggField: 'testedValue', @@ -229,7 +229,7 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: [0], }); - await createAlert({ + await createRule({ name: 'always fire', aggType: 'max', aggField: 'testedValue', @@ -264,7 +264,7 @@ export default function alertTests({ getService }: FtrProviderContext) { // create some more documents in the first group createEsDocumentsInGroups(1); - await createAlert({ + await createRule({ name: 'never fire', aggType: 'min', aggField: 'testedValue', @@ -275,7 +275,7 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: [0], }); - await createAlert({ + await createRule({ name: 'always fire', aggType: 'min', aggField: 'testedValue', @@ -306,13 +306,59 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(inGroup0).to.be.greaterThan(0); }); + it('runs correctly and populates recovery context', async () => { + // This rule should be active initially when the number of documents is below the threshold + // and then recover when we add more documents. + await createRule({ + name: 'fire then recovers', + aggType: 'count', + groupBy: 'all', + thresholdComparator: '<', + threshold: [10], + timeWindowSize: 60, + }); + + await createEsDocumentsInGroups(1); + + const docs = await waitForDocs(2); + const activeDoc = docs[0]; + const { group: activeGroup } = activeDoc._source; + const { + name: activeName, + title: activeTitle, + message: activeMessage, + } = activeDoc._source.params; + + expect(activeName).to.be('fire then recovers'); + expect(activeGroup).to.be('all documents'); + expect(activeTitle).to.be('alert fire then recovers group all documents met threshold'); + expect(activeMessage).to.match( + /alert 'fire then recovers' is active for group \'all documents\':\n\n- Value: \d+\n- Conditions Met: count is less than 10 over 60s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ + ); + + const recoveredDoc = docs[1]; + const { group: recoveredGroup } = recoveredDoc._source; + const { + name: recoveredName, + title: recoveredTitle, + message: recoveredMessage, + } = recoveredDoc._source.params; + + expect(recoveredName).to.be('fire then recovers'); + expect(recoveredGroup).to.be('all documents'); + expect(recoveredTitle).to.be('alert fire then recovers group all documents recovered'); + expect(recoveredMessage).to.match( + /alert 'fire then recovers' is recovered for group \'all documents\':\n\n- Value: \d+\n- Conditions Met: count is NOT less than 10 over 60s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ + ); + }); + async function createEsDocumentsInGroups(groups: number) { await createEsDocuments( es, esTestIndexTool, endDate, - ALERT_INTERVALS_TO_WRITE, - ALERT_INTERVAL_MILLIS, + RULE_INTERVALS_TO_WRITE, + RULE_INTERVAL_MILLIS, groups ); } @@ -325,11 +371,12 @@ export default function alertTests({ getService }: FtrProviderContext) { ); } - interface CreateAlertParams { + interface CreateRuleParams { name: string; aggType: string; aggField?: string; timeField?: string; + timeWindowSize?: number; groupBy: 'all' | 'top'; termField?: string; termSize?: number; @@ -337,9 +384,9 @@ export default function alertTests({ getService }: FtrProviderContext) { threshold: number[]; } - async function createAlert(params: CreateAlertParams): Promise { + async function createRule(params: CreateRuleParams): Promise { const action = { - id: actionId, + id: connectorId, group: 'threshold met', params: { documents: [ @@ -347,7 +394,7 @@ export default function alertTests({ getService }: FtrProviderContext) { source: ES_TEST_INDEX_SOURCE, reference: ES_TEST_INDEX_REFERENCE, params: { - name: '{{{alertName}}}', + name: '{{{rule.name}}}', value: '{{{context.value}}}', title: '{{{context.title}}}', message: '{{{context.message}}}', @@ -362,16 +409,37 @@ export default function alertTests({ getService }: FtrProviderContext) { }, }; - const { status, body: createdAlert } = await supertest + const recoveryAction = { + id: connectorId, + group: 'recovered', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{rule.name}}}', + value: '{{{context.value}}}', + title: '{{{context.title}}}', + message: '{{{context.message}}}', + }, + date: '{{{context.date}}}', + group: '{{{context.group}}}', + }, + ], + }, + }; + + const { status, body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send({ name: params.name, consumer: 'alerts', enabled: true, - rule_type_id: ALERT_TYPE_ID, - schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, - actions: [action], + rule_type_id: RULE_TYPE_ID, + schedule: { interval: `${RULE_INTERVAL_SECONDS}s` }, + actions: [action, recoveryAction], notify_when: 'onActiveAlert', params: { index: ES_TEST_INDEX_NAME, @@ -381,7 +449,7 @@ export default function alertTests({ getService }: FtrProviderContext) { groupBy: params.groupBy, termField: params.termField, termSize: params.termSize, - timeWindowSize: ALERT_INTERVAL_SECONDS * 5, + timeWindowSize: params.timeWindowSize ?? RULE_INTERVAL_SECONDS * 5, timeWindowUnit: 's', thresholdComparator: params.thresholdComparator, threshold: params.threshold, @@ -389,25 +457,25 @@ export default function alertTests({ getService }: FtrProviderContext) { }); // will print the error body, if an error occurred - // if (statusCode !== 200) console.log(createdAlert); + // if (statusCode !== 200) console.log(createdRule); expect(status).to.be(200); - const alertId = createdAlert.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); + const ruleId = createdRule.id; + objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); - return alertId; + return ruleId; } }); } -async function createAction(supertest: any, objectRemover: ObjectRemover): Promise { - const { statusCode, body: createdAction } = await supertest +async function createConnector(supertest: any, objectRemover: ObjectRemover): Promise { + const { statusCode, body: createdConnector } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ - name: 'index action for index threshold FT', - connector_type_id: ACTION_TYPE_ID, + name: 'index connector for index threshold FT', + connector_type_id: CONNECTOR_TYPE_ID, config: { index: ES_TEST_OUTPUT_INDEX_NAME, }, @@ -415,12 +483,12 @@ async function createAction(supertest: any, objectRemover: ObjectRemover): Promi }); // will print the error body, if an error occurred - // if (statusCode !== 200) console.log(createdAction); + // if (statusCode !== 200) console.log(createdConnector); expect(statusCode).to.be(200); - const actionId = createdAction.id; - objectRemover.add(Spaces.space1.id, actionId, 'connector', 'actions'); + const connectorId = createdConnector.id; + objectRemover.add(Spaces.space1.id, connectorId, 'connector', 'actions'); - return actionId; + return connectorId; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts index 77638ed90fbe4c..d9b3035ac05dd9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts @@ -29,6 +29,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { { id: 'recovered', name: 'Recovered' }, ], default_action_group_id: 'default', + does_set_recovery_context: false, id: 'test.noop', name: 'Test: Noop', action_variables: { @@ -115,6 +116,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { { id: 'recovered', name: 'Recovered' }, ], defaultActionGroupId: 'default', + doesSetRecoveryContext: false, id: 'test.noop', name: 'Test: Noop', actionVariables: { diff --git a/x-pack/test/fleet_api_integration/apis/epm/setup.ts b/x-pack/test/fleet_api_integration/apis/epm/setup.ts index eb29920b830364..253e19c2db1b1f 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/setup.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/setup.ts @@ -27,7 +27,7 @@ export default function (providerContext: FtrProviderContext) { const url = '/api/fleet/epm/packages/endpoint'; await supertest.delete(url).set('kbn-xsrf', 'xxxx').send({ force: true }).expect(200); await supertest - .post(`${url}-${oldEndpointVersion}`) + .post(`${url}/${oldEndpointVersion}`) .set('kbn-xsrf', 'xxxx') .send({ force: true }) .expect(200); @@ -52,6 +52,75 @@ export default function (providerContext: FtrProviderContext) { }); }); + describe('package policy upgrade on setup', () => { + let agentPolicyId: string; + before(async function () { + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + }); + agentPolicyId = agentPolicyResponse.item.id; + }); + + after(async function () { + await supertest + .post(`/api/fleet/agent_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ agentPolicyId }); + }); + + it('should upgrade package policy on setup if keep policies up to date set to true', async () => { + const oldVersion = '1.9.0'; + await supertest + .post(`/api/fleet/epm/packages/system/${oldVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + await supertest + .put(`/api/fleet/epm/packages/system/${oldVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ keepPoliciesUpToDate: true }) + .expect(200); + await supertest + .post('/api/fleet/package_policies') + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'system-1', + namespace: 'default', + policy_id: agentPolicyId, + package: { name: 'system', version: oldVersion }, + inputs: [], + }) + .expect(200); + + let { body } = await supertest + .get(`/api/fleet/epm/packages/system/${oldVersion}`) + .expect(200); + const latestVersion = body.item.latestVersion; + log.info(`System package latest version: ${latestVersion}`); + // make sure we're actually doing an upgrade + expect(latestVersion).not.eql(oldVersion); + + ({ body } = await supertest + .post(`/api/fleet/epm/packages/system/${latestVersion}`) + .set('kbn-xsrf', 'xxxx') + .expect(200)); + + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx').expect(200); + + ({ body } = await supertest + .get('/api/fleet/package_policies') + .set('kbn-xsrf', 'xxxx') + .expect(200)); + expect(body.items.find((pkg: any) => pkg.name === 'system-1').package.version).to.equal( + latestVersion + ); + }); + }); + it('does not fail when package is no longer compatible in registry', async () => { await supertest .post(`/api/fleet/epm/packages/deprecated/0.1.0`) diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index f2abc6b350e5b9..5d8c2aff3ed5f7 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -30,9 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }; - // Failing: See https://github.com/elastic/kibana/issues/104578 - // FLAKY: https://github.com/elastic/kibana/issues/114002 - describe.skip('Discover Saved Searches', () => { + describe('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); diff --git a/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts b/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts index 2a763014c7eb69..8d95d85a88e1e8 100644 --- a/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts +++ b/x-pack/test/functional/apps/discover/value_suggestions_non_timebased.ts @@ -9,15 +9,20 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'settings', 'context', 'header']); - const kibanaServer = getService('kibanaServer'); describe('value suggestions non time based', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded( 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' ); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/index_pattern_without_timefield' + ); + await kibanaServer.uiSettings.replace({ defaultIndex: 'without-timefield' }); await kibanaServer.uiSettings.update({ 'doc_table:legacy': true, }); @@ -27,6 +32,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.unload( 'test/functional/fixtures/es_archiver/index_pattern_without_timefield' ); + await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] }); + await kibanaServer.uiSettings.unset('defaultIndex'); await kibanaServer.uiSettings.unset('doc_table:legacy'); }); diff --git a/yarn.lock b/yarn.lock index 5b4f5d83355cc0..694f7fd4e86475 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6746,7 +6746,7 @@ "@types/parse5" "*" "@types/tough-cookie" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -7051,12 +7051,13 @@ resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5" integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow== -"@types/markdown-it@^0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39" - integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ== +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== dependencies: "@types/linkify-it" "*" + "@types/mdurl" "*" "@types/markdown-to-jsx@^6.11.3": version "6.11.3" @@ -7079,6 +7080,11 @@ dependencies: "@types/unist" "*" +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + "@types/micromatch@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7" @@ -9155,7 +9161,7 @@ async@^2.1.4, async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.1.0, async@^3.2.0: +async@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== @@ -10136,29 +10142,7 @@ browserslist@4.14.2: escalade "^3.0.2" node-releases "^1.1.61" -browserslist@^4.0.0, browserslist@^4.12.0: - version "4.17.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.1.tgz#a98d104f54af441290b7d592626dd541fa642eb9" - integrity sha512-aLD0ZMDSnF4lUt4ZDNgqi5BUn9BZ7YdQdI/cYlILrhdSSZJLU9aNZoD5/NBmM4SK34APB2e83MOsRt1EnkuyaQ== - dependencies: - caniuse-lite "^1.0.30001259" - electron-to-chromium "^1.3.846" - escalade "^3.1.1" - nanocolors "^0.1.5" - node-releases "^1.1.76" - -browserslist@^4.17.5, browserslist@^4.17.6: - version "4.18.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" - integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== - dependencies: - caniuse-lite "^1.0.30001280" - electron-to-chromium "^1.3.896" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" - -browserslist@^4.19.1: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.17.5, browserslist@^4.17.6, browserslist@^4.19.1: version "4.19.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== @@ -10501,15 +10485,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001259, caniuse-lite@^1.0.30001280: - version "1.0.30001280" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz#066a506046ba4be34cde5f74a08db7a396718fb7" - integrity sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA== - -caniuse-lite@^1.0.30001286: - version "1.0.30001303" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001303.tgz#9b168e4f43ccfc372b86f4bc5a551d9b909c95c9" - integrity sha512-/Mqc1oESndUNszJP0kx0UaQU9kEv9nNtJ7Kn8AdA0mNnH8eR1cj0kG+NbNuC1Wq/b21eA8prhKRA3bbkjONegQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001286: + version "1.0.30001309" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz#e0ee78b9bec0704f67304b00ff3c5c0c768a9f62" + integrity sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA== canvg@^3.0.9: version "3.0.9" @@ -10718,7 +10697,7 @@ cheerio@^1.0.0-rc.10, cheerio@^1.0.0-rc.3: parse5-htmlparser2-tree-adapter "^6.0.1" tslib "^2.2.0" -chokidar@3.4.3, chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.2, chokidar@^2.1.8, chokidar@^3.2.2, chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.4.3: +chokidar@3.4.3, chokidar@^2.0.0, chokidar@^2.0.4, chokidar@^2.1.2, chokidar@^2.1.8, chokidar@^3.4.0, chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.4.3, chokidar@^3.5.2: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== @@ -11200,12 +11179,7 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" -colorette@^1.2.0, colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - -colorette@^1.2.2: +colorette@^1.2.0, colorette@^1.2.1, colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== @@ -11215,7 +11189,7 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@1.4.0, colors@^1.1.2, colors@^1.2.1, colors@^1.3.2: +colors@1.4.0, colors@^1.1.2, colors@^1.3.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -11371,7 +11345,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: +concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -12699,17 +12673,17 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@3.X, debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6, debug@^3.2.7: +debug@3.X, debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@4, debug@4.3.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@4, debug@4.3.3, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" @@ -12734,17 +12708,17 @@ debug@4.2.0: dependencies: ms "2.1.2" -debug@4.3.2, debug@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" -debug@4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== +debug@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" @@ -13674,25 +13648,10 @@ elasticsearch@^16.4.0: chalk "^1.0.0" lodash "^4.17.10" -electron-to-chromium@^1.3.564: - version "1.3.642" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.642.tgz#8b884f50296c2ae2a9997f024d0e3e57facc2b94" - integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ== - -electron-to-chromium@^1.3.846: - version "1.3.853" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.853.tgz#f3ed1d31f092cb3a17af188bca6c6a3ec91c3e82" - integrity sha512-W4U8n+U8I5/SUaFcqZgbKRmYZwcyEIQVBDf+j5QQK6xChjXnQD+wj248eGR9X4u+dDmDR//8vIfbu4PrdBBIoQ== - -electron-to-chromium@^1.3.896: - version "1.3.899" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.899.tgz#4d7d040e73def3d5f5bd6b8a21049025dce6fce0" - integrity sha512-w16Dtd2zl7VZ4N4Db+FIa7n36sgPGCKjrKvUUmp5ialsikvcQLjcJR9RWnlYNxIyEHLdHaoIZEqKsPxU9MdyBg== - -electron-to-chromium@^1.4.17: - version "1.4.57" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz#2b2766df76ac8dbc0a1d41249bc5684a31849892" - integrity sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw== +electron-to-chromium@^1.3.564, electron-to-chromium@^1.4.17: + version "1.4.66" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.66.tgz#d7453d363dcd7b06ed1757adcde34d724e27b367" + integrity sha512-f1RXFMsvwufWLwYUxTiP7HmjprKXrqEWHiQkjAYa9DJeVIlZk5v8gBGcaV+FhtXLly6C1OTVzQY+2UQrACiLlg== elegant-spinner@^1.0.1: version "1.0.1" @@ -13849,6 +13808,11 @@ entities@~2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + env-paths@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -14600,9 +14564,9 @@ events@^2.0.0: integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg== events@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" - integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@^1.0.7: version "1.0.7" @@ -14970,7 +14934,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.0.tgz#ac2f9e36c9f4976f5db9fb18c6ffbaf308cf316d" integrity sha512-a/S/Hp6aoIjx7EmugtzLqXmcNsyFszqbt6qQ99BdG61QjBZF6shNis0BYR6TsZOQ1twYc0FN2Xdhwwbv6+KD0w== -fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: +fast-safe-stringify@^2.0.7: version "2.0.8" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== @@ -15384,20 +15348,10 @@ follow-redirects@1.12.1: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== -follow-redirects@^1.0.0, follow-redirects@^1.10.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" - integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== - -follow-redirects@^1.14.4: - version "1.14.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" - integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== - -follow-redirects@^1.14.7: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +follow-redirects@^1.0.0, follow-redirects@^1.10.0, follow-redirects@^1.14.4, follow-redirects@^1.14.7: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== font-awesome@4.7.0: version "4.7.0" @@ -15594,17 +15548,7 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0, fs-extra@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^1.0.0" - -fs-extra@^9.1.0: +fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -15985,9 +15929,9 @@ glob-to-regexp@^0.3.0: integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= glob-to-regexp@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz#49bd677b1671022bd10921c3788f23cdebf9c7e6" - integrity sha512-fyPCII4vn9Gvjq2U/oDAfP433aiE64cyP/CJjRJcpVGjqqNdioUYn9+r0cSzT1XPwmGAHuTT7iv+rQT8u/YHKQ== + version "0.4.1" + 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: version "5.0.3" @@ -16001,7 +15945,7 @@ glob-watcher@5.0.3, glob-watcher@^5.0.3: just-debounce "^1.0.0" object.defaults "^1.1.0" -glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1, glob@~7.1.4: +glob@7.1.6, glob@~7.1.1, glob@~7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -16024,6 +15968,18 @@ glob@^6.0.1, glob@^6.0.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" @@ -17170,14 +17126,7 @@ iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" - integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@^0.6.3: +iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -19042,9 +18991,9 @@ jpeg-js@^0.4.2: integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== jquery@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.0.tgz#9980b97d9e4194611c36530e7dc46a58d7340fc9" - integrity sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ== + version "3.6.0" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" + integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== js-base64@^2.1.8: version "2.4.5" @@ -19707,13 +19656,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -linkify-it@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f" - integrity sha1-2UpGSPmxwXnWT6lykSaL22zpQ08= - dependencies: - uc.micro "^1.0.1" - linkify-it@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" @@ -20126,17 +20068,6 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -logform@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" - integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== - dependencies: - colors "^1.2.1" - fast-safe-stringify "^2.0.4" - fecha "^4.2.0" - ms "^2.1.1" - triple-beam "^1.3.0" - logform@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/logform/-/logform-2.3.2.tgz#68babe6a74ab09a1fd15a9b1e6cbc7713d41cb5b" @@ -20406,17 +20337,6 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.1.tgz#1994df2d3af4811de59a6714934c2b2292734518" integrity sha1-GZTfLTr0gR3lmmcUk0wrIpJzRRg= -markdown-it@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" - integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg== - dependencies: - argparse "^1.0.7" - entities "~2.0.0" - linkify-it "^2.0.0" - mdurl "^1.0.1" - uc.micro "^1.0.5" - markdown-it@^11.0.0: version "11.0.1" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.1.tgz#b54f15ec2a2193efa66dda1eb4173baea08993d6" @@ -20428,6 +20348,17 @@ markdown-it@^11.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + markdown-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" @@ -21373,11 +21304,6 @@ nano-time@1.0.0: dependencies: big-integer "^1.6.16" -nanocolors@^0.1.5: - version "0.1.12" - resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.1.12.tgz#8577482c58cbd7b5bb1681db4cf48f11a87fd5f6" - integrity sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ== - nanoid@3.1.12: version "3.1.12" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.12.tgz#6f7736c62e8d39421601e4a0c77623a97ea69654" @@ -21693,11 +21619,6 @@ node-releases@^1.1.61: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== -node-releases@^1.1.76: - version "1.1.76" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.76.tgz#df245b062b0cafbd5282ab6792f7dccc2d97f36e" - integrity sha512-9/IECtNr8dXNmPWmFXepT0/7o5eolGesHUa3mtr0KlgnCvnZxwh2qensKL42JJY2vQKC3nIBXetFAqR+PW1CmA== - node-releases@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" @@ -21730,20 +21651,20 @@ nodemailer@^6.6.2: integrity sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q== nodemon@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.6.tgz#1abe1937b463aaf62f0d52e2b7eaadf28cc2240d" - integrity sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ== + version "2.0.15" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.15.tgz#504516ce3b43d9dc9a955ccd9ec57550a31a8d4e" + integrity sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA== dependencies: - chokidar "^3.2.2" - debug "^3.2.6" + chokidar "^3.5.2" + debug "^3.2.7" ignore-by-default "^1.0.1" minimatch "^3.0.4" - pstree.remy "^1.1.7" + pstree.remy "^1.1.8" semver "^5.7.1" supports-color "^5.5.0" touch "^3.1.0" - undefsafe "^2.0.3" - update-notifier "^4.1.0" + undefsafe "^2.0.5" + update-notifier "^5.1.0" nopt@^2.2.0: version "2.2.1" @@ -22168,9 +22089,9 @@ onetime@^2.0.0: mimic-fn "^1.0.0" onetime@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" - integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" @@ -24013,10 +23934,10 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== -pstree.remy@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" - integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== public-encrypt@^4.0.0: version "4.0.0" @@ -25153,7 +25074,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", "readable-stream@2 || 3", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.7, readable-stream@~2.3.3, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -25176,7 +25097,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0 isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@3, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -26334,11 +26255,11 @@ schema-utils@^1.0.0: ajv-keywords "^3.1.0" schema-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" - integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== dependencies: - "@types/json-schema" "^7.0.6" + "@types/json-schema" "^7.0.8" ajv "^6.12.5" ajv-keywords "^3.5.2" @@ -26445,12 +26366,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2, semver@~7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - -semver@^7.3.4, semver@^7.3.5, semver@~7.3.0: +semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@~7.3.0, semver@~7.3.2: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -26722,9 +26638,9 @@ sigmund@^1.0.1: integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== simple-concat@^1.0.0: version "1.0.1" @@ -27011,18 +26927,10 @@ source-map-support@^0.3.2: dependencies: source-map "0.1.32" -source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.20: - version "0.5.20" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== +source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.20, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.19, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -28382,13 +28290,13 @@ terser@^4.1.2, terser@^4.6.3: source-map-support "~0.5.12" terser@^5.3.4, terser@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784" - integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg== + version "5.10.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" + integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== dependencies: commander "^2.20.0" source-map "~0.7.2" - source-map-support "~0.5.19" + source-map-support "~0.5.20" test-exclude@^6.0.0: version "6.0.0" @@ -28811,7 +28719,7 @@ trim@0.0.1, trim@1.0.1: resolved "https://registry.yarnpkg.com/trim/-/trim-1.0.1.tgz#68e78f6178ccab9687a610752f4f5e5a7022ee8c" integrity sha512-3JVP2YVqITUisXblCDq/Bi4P9457G/sdEamInkyvCsjbTcXLXIiG7XCb4kGMFWh6JGXesS3TKxOPtrncN/xe8w== -triple-beam@^1.2.0, triple-beam@^1.3.0: +triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== @@ -29180,12 +29088,10 @@ undeclared-identifiers@^1.1.2: simple-concat "^1.0.0" xtend "^4.0.1" -undefsafe@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" - integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== - dependencies: - debug "^2.2.0" +undefsafe@^2.0.5: + version "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: version "1.13.1" @@ -29651,9 +29557,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3, url-parse@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" - integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== + version "1.5.9" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.9.tgz#05ff26484a0b5e4040ac64dcee4177223d74675e" + integrity sha512-HpOvhKBvre8wYez+QhHcYiVvVmeF6DVnuSOOPhe3cTum3BnqHhvKaZm8FU5yTiOu/Jut2ZpB2rA/SbBA1JIGlQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -30877,14 +30783,6 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" -winston-transport@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" - integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== - dependencies: - readable-stream "^2.3.7" - triple-beam "^1.2.0" - winston-transport@^4.4.2: version "4.5.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" @@ -30894,22 +30792,7 @@ winston-transport@^4.4.2: readable-stream "^3.6.0" triple-beam "^1.3.0" -winston@^3.0.0, winston@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" - integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== - dependencies: - "@dabh/diagnostics" "^2.0.2" - async "^3.1.0" - is-stream "^2.0.0" - logform "^2.2.0" - one-time "^1.0.0" - readable-stream "^3.4.0" - stack-trace "0.0.x" - triple-beam "^1.3.0" - winston-transport "^4.4.0" - -winston@^3.5.1: +winston@^3.0.0, winston@^3.3.3, winston@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/winston/-/winston-3.5.1.tgz#b25cc899d015836dbf8c583dec8c4c4483a0da2e" integrity sha512-tbRtVy+vsSSCLcZq/8nXZaOie/S2tPXPFt4be/Q3vI/WtYwm7rrwidxVw2GRa38FIXcJ1kUM6MOZ9Jmnk3F3UA== @@ -31310,20 +31193,7 @@ yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.0.1: - version "17.1.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.1.tgz#c2a8091564bdb196f7c0a67c1d12e5b85b8067ba" - integrity sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.3.1: +yargs@^17.0.1, yargs@^17.3.1: version "17.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==