diff --git a/package.json b/package.json index 96eda869305c6..3b3d2d103263b 100644 --- a/package.json +++ b/package.json @@ -292,7 +292,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 +631,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 d390966a6a52f..647d421819597 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 c1e026064fdfb..0adb06d91d268 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 28361114be6e1..fa9b7ac86a7fa 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 f01401948dec8..5c0c2102f804c 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 8b90f92b932e0..0458a940482de 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 4b7b447d2c8be..ce6f2e03744fa 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 f9f77ee0ca12f..98db107f32a11 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 e6e48c477ebc9..4bee00f3c4b2a 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 5b431424c1b44..bd4f62b2c55f3 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 c55385e152bcf..e921d0beafce1 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 a4a09562c300f..981654ac52d91 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 fe7e8c57cd4ec..eba7d19a5a0d0 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 6d6e1af7b2fbc..2d5892fa6e6ca 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 6a7d414becfe7..117b8747c5a0a 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 0888079ec7c52..9998cb3a71732 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 c230ff8ff7e39..0000000000000 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 f4962f9c47668..0000000000000 --- 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 95b32f0ee11e5..0000000000000 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 451369d85acd8..0000000000000 --- 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 c8d1c98790e59..0000000000000 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 451369d85acd8..0000000000000 --- 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 238ffe3b76241..0000000000000 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 ec6a9ce7f13a1..0000000000000 --- 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 0000000000000..d5906dc8a2e99 --- /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 bc917fbf43bc4..8fdfe77776b4e 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 6f5f00e8f4073..eb24e29f552b9 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 83b82de904703..eae1b18164b0f 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 d34aa68ac1a11..bf29cacf556c1 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 ecb1a10bbac42..254da05c0dd53 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 07f4dbc7b20ea..ad83b0a416c72 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 5e1a9ee626b57..2b5dc4791037e 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 0000000000000..b984b04fc65d4 --- /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 0000000000000..f389f56a813d0 --- /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 29526f17268f2..a5fa1b29c3044 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 afbc3ef9cec43..f7872ba797856 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 70aad0d6921e1..ac3253346138a 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 7deb2704fb7ec..752f729fb8e38 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 d1f24538d76d8..7b2a0c63be198 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 e23c7f25a4f76..8ba2847486bca 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 9b4f94f3510be..6673fb630ef59 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 63e35583bc9a1..72ef2dba89ce7 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 51d50c398c6f5..8bc4ad280873e 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 9d6302774f889..50acb67a3de47 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 f881b4476fe22..a34b3cdb1334d 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 f72b176bd8d7b..65485a31d376a 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 29a491fe0c932..b5872b0a995a9 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 9c6319a92b2ee..8946e6af0ce75 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 ba3af7716d985..b73bd7251f853 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 aed6c34712012..29ea4102bc1ef 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 87382ac70a9bb..a8354237bbcb7 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 22b58c14fb072..5952fbebcf272 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 385ef2bee6512..79dc8cc38c4bf 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 4bc06d51e7e0b..272e92fca6eae 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 af3b3ef4dfccd..402b65334121a 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 77715ad488feb..2c4b326d56532 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 e9c079d435e7e..2e0c3c7722b13 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 caa2e1b3ffcee..02972648e80d2 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 b99b73be8588c..e7ba627f5cbdf 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 bcce2fa2f6f69..515f5a40fd58a 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 3c1297e864553..c37ab22002c1c 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 8c75ee9efcc6b..6cab22cd08ccd 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 5d475be7bb83f..ccd9e8aace2ab 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 3554f77047577..a26d72f1b4fc2 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 5ae3cb571bdbb..c1c86367ee211 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 712e9f9f7f476..aa0a9de248c1b 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 404c31010278b..2c74f8468e52b 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 2a44550af2b58..0ac77696d5987 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 26cbd2a990978..beca7cfa4c39f 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 ea43766464cf5..e269778b5ad53 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 0d8b57a5502ad..f9fe8701949e1 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 0000000000000..857f30e692305 --- /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 921cc8fb364a2..f71f7a128934a 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 bc57547bc0ee6..6bee021b36de6 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 9594d8920515b..ea0e336ff2f08 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 d5eb8ac1e92ba..81037418a8143 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/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 baa60664dea57..3593030913ba7 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 79a2314af9397..1b9c63dd2dbcb 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 62ba50a494df5..05b9cb567fafd 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 0887c4774f9a8..32ce0bebda225 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 16beb418d0d13..2d5a676646688 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 ce232f7b84157..436ef0975ef02 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 bdd83d93fa25d..288d16dc22fb6 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 efa7f31455b1f..af2772b98a790 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 0e4dbc9a95f9c..c5e015b6382c2 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 033a559ffff98..e8873de412f4c 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 20a2f6ebed3e2..ece7dbe559672 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 530ec4934b447..b98f626c6356c 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 c3797cd4b178c..8384c879d8110 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 da8b089ee186d..d34d9bd4fc171 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 e0596d52809e0..bf8d753a8161c 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 6d078b3da24c0..694036d8a1678 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 ec84359e63712..921128ce3303d 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 2e5994e73ac52..9887eb1e8612b 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 1262bea01708d..d9661324aee6d 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 d718b499f199d..a30b651bfba39 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 775817dcb8a0c..11396864d802d 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 3d96e3bb77907..3d390cac6b91f 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 7cc709bbe8994..88d6114387aa3 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 a625d9c193cd6..402d6fca7a1a9 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 9a5e0de9d53bf..4d8c1dc3d9b9f 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 69ca1c2700ebe..02450da5bbdf7 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 0eb2810626ac3..7725721ed8efa 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/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 30e594f35d1f8..c2ab2936e0cc7 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 2cf1df85a3447..6aff0781a56c2 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 99478f250f6a2..63207dd35dfac 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 836e63fa7a68a..f25827fb4ba99 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 4a4230c233dfa..4a27c4c1e6fef 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 06542cbb3a1a4..fa226c4a74cdd 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 72fd48d355774..718a637518cbe 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 b070219410fd9..0c527ac1449f8 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 8c796c1a39d67..ba2a2cb8fdf47 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 77638ed90fbe4..d9b3035ac05dd 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 eb29920b83036..253e19c2db1b1 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 f2abc6b350e5b..5d8c2aff3ed5f 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 2a763014c7eb6..8d95d85a88e1e 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 d907d69e4c539..585056202e038 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.5.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.5.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== @@ -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" @@ -12727,17 +12701,17 @@ debug@4.1.1: dependencies: ms "^2.1.1" -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" @@ -13672,25 +13646,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" @@ -13847,6 +13806,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" @@ -14598,9 +14562,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" @@ -14968,7 +14932,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== @@ -15382,20 +15346,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" @@ -15601,17 +15555,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== @@ -15992,9 +15936,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" @@ -16008,7 +15952,7 @@ glob-watcher@5.0.3, glob-watcher@^5.0.3: just-debounce "^1.0.0" object.defaults "^1.1.0" -glob@7.2.0: +glob@7.2.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== @@ -16031,7 +15975,7 @@ 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, glob@~7.1.1, glob@~7.1.4: +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== @@ -17189,14 +17133,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== @@ -19061,9 +18998,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" @@ -19726,13 +19663,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" @@ -20145,17 +20075,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" @@ -20425,17 +20344,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" @@ -20447,6 +20355,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" @@ -21390,11 +21309,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.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" @@ -21710,11 +21624,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" @@ -21747,20 +21656,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" @@ -22185,9 +22094,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" @@ -24025,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" @@ -25165,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== @@ -25188,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== @@ -26346,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" @@ -26457,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== @@ -26741,9 +26645,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" @@ -27030,18 +26934,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" @@ -28394,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" @@ -28823,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== @@ -29192,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" @@ -29663,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" @@ -30872,14 +30766,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" @@ -30889,22 +30775,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== @@ -31318,20 +31189,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.2.1, yargs@^17.3.1: +yargs@^17.0.1, yargs@^17.2.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==