* [eslint] prevent async Promise constructor mistakes (#110349) Co-authored-by: spalger <spalger@users.noreply.github.com> * autofix one more violation Co-authored-by: Spencer <email@spalger.com> Co-authored-by: spalger <spalger@users.noreply.github.com>
This commit is contained in:
parent
0824bd8c92
commit
2ee5e66b26
|
@ -657,6 +657,7 @@
|
|||
"@types/yauzl": "^2.9.1",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.1",
|
||||
"@typescript-eslint/typescript-estree": "^4.14.1",
|
||||
"@typescript-eslint/parser": "^4.14.1",
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"abab": "^2.0.4",
|
||||
|
@ -727,6 +728,7 @@
|
|||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-react-perf": "^3.2.3",
|
||||
"eslint-traverse": "^1.0.0",
|
||||
"expose-loader": "^0.7.5",
|
||||
"faker": "^5.1.0",
|
||||
"fancy-log": "^1.3.2",
|
||||
|
|
|
@ -90,5 +90,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
],
|
||||
|
||||
'@kbn/eslint/no_async_promise_body': 'error',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,5 +12,6 @@ module.exports = {
|
|||
'disallow-license-headers': require('./rules/disallow_license_headers'),
|
||||
'no-restricted-paths': require('./rules/no_restricted_paths'),
|
||||
module_migration: require('./rules/module_migration'),
|
||||
no_async_promise_body: require('./rules/no_async_promise_body'),
|
||||
},
|
||||
};
|
||||
|
|
165
packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js
Normal file
165
packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { parseExpression } = require('@babel/parser');
|
||||
const { default: generate } = require('@babel/generator');
|
||||
const tsEstree = require('@typescript-eslint/typescript-estree');
|
||||
const traverse = require('eslint-traverse');
|
||||
const esTypes = tsEstree.AST_NODE_TYPES;
|
||||
const babelTypes = require('@babel/types');
|
||||
|
||||
/** @typedef {import("eslint").Rule.RuleModule} Rule */
|
||||
/** @typedef {import("@typescript-eslint/parser").ParserServices} ParserServices */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Expression} Expression */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ArrowFunctionExpression} ArrowFunctionExpression */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.FunctionExpression} FunctionExpression */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.TryStatement} TryStatement */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.NewExpression} NewExpression */
|
||||
/** @typedef {import("typescript").ExportDeclaration} ExportDeclaration */
|
||||
/** @typedef {import("eslint").Rule.RuleFixer} Fixer */
|
||||
|
||||
const ERROR_MSG =
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections';
|
||||
|
||||
/**
|
||||
* @param {Expression} node
|
||||
*/
|
||||
const isPromise = (node) => node.type === esTypes.Identifier && node.name === 'Promise';
|
||||
|
||||
/**
|
||||
* @param {Expression} node
|
||||
* @returns {node is ArrowFunctionExpression | FunctionExpression}
|
||||
*/
|
||||
const isFunc = (node) =>
|
||||
node.type === esTypes.ArrowFunctionExpression || node.type === esTypes.FunctionExpression;
|
||||
|
||||
/**
|
||||
* @param {any} context
|
||||
* @param {ArrowFunctionExpression | FunctionExpression} node
|
||||
*/
|
||||
const isFuncBodySafe = (context, node) => {
|
||||
// if the body isn't wrapped in a blockStatement it can't have a try/catch at the root
|
||||
if (node.body.type !== esTypes.BlockStatement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// when the entire body is wrapped in a try/catch it is the only node
|
||||
if (node.body.body.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tryNode = node.body.body[0];
|
||||
// ensure we have a try node with a handler
|
||||
if (tryNode.type !== esTypes.TryStatement || !tryNode.handler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ensure the handler doesn't throw
|
||||
let hasThrow = false;
|
||||
traverse(context, tryNode.handler, (path) => {
|
||||
if (path.node.type === esTypes.ThrowStatement) {
|
||||
hasThrow = true;
|
||||
return traverse.STOP;
|
||||
}
|
||||
});
|
||||
return !hasThrow;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
*/
|
||||
const wrapFunctionInTryCatch = (code) => {
|
||||
// parse the code with babel so we can mutate the AST
|
||||
const ast = parseExpression(code, {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
});
|
||||
|
||||
// validate that the code reperesents an arrow or function expression
|
||||
if (!babelTypes.isArrowFunctionExpression(ast) && !babelTypes.isFunctionExpression(ast)) {
|
||||
throw new Error('expected function to be an arrow or function expression');
|
||||
}
|
||||
|
||||
// ensure that the function receives the second argument, and capture its name if already defined
|
||||
let rejectName = 'reject';
|
||||
if (ast.params.length === 0) {
|
||||
ast.params.push(babelTypes.identifier('resolve'), babelTypes.identifier(rejectName));
|
||||
} else if (ast.params.length === 1) {
|
||||
ast.params.push(babelTypes.identifier(rejectName));
|
||||
} else if (ast.params.length === 2) {
|
||||
if (babelTypes.isIdentifier(ast.params[1])) {
|
||||
rejectName = ast.params[1].name;
|
||||
} else {
|
||||
throw new Error('expected second param of promise definition function to be an identifier');
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the body of the function is a blockStatement
|
||||
let block = ast.body;
|
||||
if (!babelTypes.isBlockStatement(block)) {
|
||||
block = babelTypes.blockStatement([babelTypes.returnStatement(block)]);
|
||||
}
|
||||
|
||||
// redefine the body of the function as a new blockStatement containing a tryStatement
|
||||
// which catches errors and forwards them to reject() when caught
|
||||
ast.body = babelTypes.blockStatement([
|
||||
// try {
|
||||
babelTypes.tryStatement(
|
||||
block,
|
||||
// catch (error) {
|
||||
babelTypes.catchClause(
|
||||
babelTypes.identifier('error'),
|
||||
babelTypes.blockStatement([
|
||||
// reject(error)
|
||||
babelTypes.expressionStatement(
|
||||
babelTypes.callExpression(babelTypes.identifier(rejectName), [
|
||||
babelTypes.identifier('error'),
|
||||
])
|
||||
),
|
||||
])
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
return generate(ast).code;
|
||||
};
|
||||
|
||||
/** @type {Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: (context) => ({
|
||||
NewExpression(_) {
|
||||
const node = /** @type {NewExpression} */ (_);
|
||||
|
||||
// ensure we are newing up a promise with a single argument
|
||||
if (!isPromise(node.callee) || node.arguments.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const func = node.arguments[0];
|
||||
// ensure the argument is an arrow or function expression and is async
|
||||
if (!isFunc(func) || !func.async) {
|
||||
return;
|
||||
}
|
||||
|
||||
// body must be a blockStatement, try/catch can't exist outside of a block
|
||||
if (!isFuncBodySafe(context, func)) {
|
||||
context.report({
|
||||
message: ERROR_MSG,
|
||||
loc: func.loc,
|
||||
fix(fixer) {
|
||||
const source = context.getSourceCode();
|
||||
return fixer.replaceText(func, wrapFunctionInTryCatch(source.getText(func)));
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const rule = require('./no_async_promise_body');
|
||||
const dedent = require('dedent');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('@kbn/eslint/no_async_promise_body', rule, {
|
||||
valid: [
|
||||
// caught but no resolve
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async function (resolve) {
|
||||
try {
|
||||
await asyncOperation();
|
||||
} catch (error) {
|
||||
// noop
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// arrow caught but no resolve
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async (resolve) => {
|
||||
try {
|
||||
await asyncOperation();
|
||||
} catch (error) {
|
||||
// noop
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// caught with reject
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async function (resolve, reject) {
|
||||
try {
|
||||
await asyncOperation();
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// arrow caught with reject
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await asyncOperation();
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// non async
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(function (resolve) {
|
||||
setTimeout(resolve, 10);
|
||||
})
|
||||
`,
|
||||
},
|
||||
// arrow non async
|
||||
{
|
||||
code: dedent`
|
||||
new Promise((resolve) => setTimeout(resolve, 10))
|
||||
`,
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
// no catch
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async function (resolve) {
|
||||
const result = await asyncOperation();
|
||||
resolve(result);
|
||||
})
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
|
||||
},
|
||||
],
|
||||
output: dedent`
|
||||
new Promise(async function (resolve, reject) {
|
||||
try {
|
||||
const result = await asyncOperation();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// arrow no catch
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async (resolve) => {
|
||||
const result = await asyncOperation();
|
||||
resolve(result);
|
||||
})
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
|
||||
},
|
||||
],
|
||||
output: dedent`
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const result = await asyncOperation();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// catch, but it throws
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async function (resolve) {
|
||||
try {
|
||||
const result = await asyncOperation();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
if (error.code === 'foo') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
})
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
|
||||
},
|
||||
],
|
||||
output: dedent`
|
||||
new Promise(async function (resolve, reject) {
|
||||
try {
|
||||
try {
|
||||
const result = await asyncOperation();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
if (error.code === 'foo') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
// no catch without block
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async (resolve) => resolve(await asyncOperation()));
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
|
||||
},
|
||||
],
|
||||
output: dedent`
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
return resolve(await asyncOperation());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
// no catch with named reject
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async (resolve, rej) => {
|
||||
const result = await asyncOperation();
|
||||
result ? resolve(true) : rej()
|
||||
});
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
|
||||
},
|
||||
],
|
||||
output: dedent`
|
||||
new Promise(async (resolve, rej) => {
|
||||
try {
|
||||
const result = await asyncOperation();
|
||||
result ? resolve(true) : rej();
|
||||
} catch (error) {
|
||||
rej(error);
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
// no catch with no args
|
||||
{
|
||||
code: dedent`
|
||||
new Promise(async () => {
|
||||
await asyncOperation();
|
||||
});
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections',
|
||||
},
|
||||
],
|
||||
output: dedent`
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await asyncOperation();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
|
@ -21,13 +21,18 @@ export const createRenderer = (element: ReactElement | null): Renderer => {
|
|||
const dom: Dom = element && mount(<I18nProvider>{element}</I18nProvider>);
|
||||
|
||||
return () =>
|
||||
new Promise(async (resolve) => {
|
||||
if (dom) {
|
||||
await act(async () => {
|
||||
dom.update();
|
||||
});
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (dom) {
|
||||
await act(async () => {
|
||||
dom.update();
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(() => resolve(dom)); // flushes any pending promises
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
setImmediate(() => resolve(dom)); // flushes any pending promises
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -27,8 +27,12 @@ describe('AppContainer', () => {
|
|||
});
|
||||
|
||||
const flushPromises = async () => {
|
||||
await new Promise<void>(async (resolve) => {
|
||||
setImmediate(() => resolve());
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
setImmediate(() => resolve());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -26,13 +26,18 @@ describe('HeaderActionMenu', () => {
|
|||
});
|
||||
|
||||
const refresh = () => {
|
||||
new Promise(async (resolve) => {
|
||||
if (component) {
|
||||
act(() => {
|
||||
component.update();
|
||||
});
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (component) {
|
||||
act(() => {
|
||||
component.update();
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(() => resolve(component)); // flushes any pending promises
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
setImmediate(() => resolve(component)); // flushes any pending promises
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -25,42 +25,47 @@ describe.skip('useActiveCursor', () => {
|
|||
events: Array<Partial<ActiveCursorPayload>>,
|
||||
eventsTimeout = 1
|
||||
) =>
|
||||
new Promise(async (resolve) => {
|
||||
const activeCursor = new ActiveCursor();
|
||||
let allEventsExecuted = false;
|
||||
|
||||
activeCursor.setup();
|
||||
|
||||
dispatchExternalPointerEvent.mockImplementation((pointerEvent) => {
|
||||
if (allEventsExecuted) {
|
||||
resolve(pointerEvent);
|
||||
}
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useActiveCursor(
|
||||
activeCursor,
|
||||
{
|
||||
current: {
|
||||
dispatchExternalPointerEvent: dispatchExternalPointerEvent as (
|
||||
pointerEvent: PointerEvent
|
||||
) => void,
|
||||
},
|
||||
} as RefObject<Chart>,
|
||||
{ ...syncOption, debounce: syncOption.debounce ?? 1 }
|
||||
)
|
||||
);
|
||||
|
||||
for (const e of events) {
|
||||
await new Promise((eventResolve) =>
|
||||
setTimeout(() => {
|
||||
if (e === events[events.length - 1]) {
|
||||
allEventsExecuted = true;
|
||||
}
|
||||
activeCursor.activeCursor$!.next({ cursor, ...e });
|
||||
eventResolve(null);
|
||||
}, eventsTimeout)
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const activeCursor = new ActiveCursor();
|
||||
let allEventsExecuted = false;
|
||||
activeCursor.setup();
|
||||
dispatchExternalPointerEvent.mockImplementation((pointerEvent) => {
|
||||
if (allEventsExecuted) {
|
||||
resolve(pointerEvent);
|
||||
}
|
||||
});
|
||||
renderHook(() =>
|
||||
useActiveCursor(
|
||||
activeCursor,
|
||||
{
|
||||
current: {
|
||||
dispatchExternalPointerEvent: dispatchExternalPointerEvent as (
|
||||
pointerEvent: PointerEvent
|
||||
) => void,
|
||||
},
|
||||
} as RefObject<Chart>,
|
||||
{ ...syncOption, debounce: syncOption.debounce ?? 1 }
|
||||
)
|
||||
);
|
||||
|
||||
for (const e of events) {
|
||||
await new Promise((eventResolve) =>
|
||||
setTimeout(() => {
|
||||
if (e === events[events.length - 1]) {
|
||||
allEventsExecuted = true;
|
||||
}
|
||||
|
||||
activeCursor.activeCursor$!.next({
|
||||
cursor,
|
||||
...e,
|
||||
});
|
||||
eventResolve(null);
|
||||
}, eventsTimeout)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -19,13 +19,18 @@ describe('MountPointPortal', () => {
|
|||
let dom: ReactWrapper;
|
||||
|
||||
const refresh = () => {
|
||||
new Promise(async (resolve) => {
|
||||
if (dom) {
|
||||
act(() => {
|
||||
dom.update();
|
||||
});
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (dom) {
|
||||
act(() => {
|
||||
dom.update();
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(() => resolve(dom)); // flushes any pending promises
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
setImmediate(() => resolve(dom)); // flushes any pending promises
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -16,10 +16,14 @@ export async function getServiceSettings(): Promise<IServiceSettings> {
|
|||
return loadPromise;
|
||||
}
|
||||
|
||||
loadPromise = new Promise(async (resolve) => {
|
||||
const { ServiceSettings } = await import('./lazy');
|
||||
const config = getMapsEmsConfig();
|
||||
resolve(new ServiceSettings(config, config.tilemap));
|
||||
loadPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { ServiceSettings } = await import('./lazy');
|
||||
const config = getMapsEmsConfig();
|
||||
resolve(new ServiceSettings(config, config.tilemap));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return loadPromise;
|
||||
}
|
||||
|
|
|
@ -18,13 +18,16 @@ export async function lazyLoadMapsLegacyModules(): Promise<LazyLoadedMapsLegacyM
|
|||
return loadModulesPromise;
|
||||
}
|
||||
|
||||
loadModulesPromise = new Promise(async (resolve) => {
|
||||
const { KibanaMap, L } = await import('./lazy');
|
||||
|
||||
resolve({
|
||||
KibanaMap,
|
||||
L,
|
||||
});
|
||||
loadModulesPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { KibanaMap, L } = await import('./lazy');
|
||||
resolve({
|
||||
KibanaMap,
|
||||
L,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return loadModulesPromise;
|
||||
}
|
||||
|
|
|
@ -109,13 +109,18 @@ describe('TopNavMenu', () => {
|
|||
let dom: ReactWrapper;
|
||||
|
||||
const refresh = () => {
|
||||
new Promise(async (resolve) => {
|
||||
if (dom) {
|
||||
act(() => {
|
||||
dom.update();
|
||||
});
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (dom) {
|
||||
act(() => {
|
||||
dom.update();
|
||||
});
|
||||
}
|
||||
|
||||
setImmediate(() => resolve(dom)); // flushes any pending promises
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
setImmediate(() => resolve(dom)); // flushes any pending promises
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -71,35 +71,42 @@ export function getTableVisualizationControllerClass(
|
|||
await this.initLocalAngular();
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!this.$rootScope) {
|
||||
const $injector = this.getInjector();
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.$compile = $injector.get('$compile');
|
||||
}
|
||||
const updateScope = () => {
|
||||
if (!this.$scope) {
|
||||
return;
|
||||
try {
|
||||
if (!this.$rootScope) {
|
||||
const $injector = this.getInjector();
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.$compile = $injector.get('$compile');
|
||||
}
|
||||
|
||||
this.$scope.visState = { params: visParams, title: visParams.title };
|
||||
this.$scope.esResponse = esResponse;
|
||||
const updateScope = () => {
|
||||
if (!this.$scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$scope.visParams = visParams;
|
||||
this.$scope.renderComplete = resolve;
|
||||
this.$scope.renderFailed = reject;
|
||||
this.$scope.resize = Date.now();
|
||||
this.$scope.$apply();
|
||||
};
|
||||
this.$scope.visState = {
|
||||
params: visParams,
|
||||
title: visParams.title,
|
||||
};
|
||||
this.$scope.esResponse = esResponse;
|
||||
this.$scope.visParams = visParams;
|
||||
this.$scope.renderComplete = resolve;
|
||||
this.$scope.renderFailed = reject;
|
||||
this.$scope.resize = Date.now();
|
||||
this.$scope.$apply();
|
||||
};
|
||||
|
||||
if (!this.$scope && this.$compile) {
|
||||
this.$scope = this.$rootScope.$new();
|
||||
this.$scope.uiState = handlers.uiState;
|
||||
this.$scope.filter = handlers.event;
|
||||
updateScope();
|
||||
this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope));
|
||||
this.$scope.$apply();
|
||||
} else {
|
||||
updateScope();
|
||||
if (!this.$scope && this.$compile) {
|
||||
this.$scope = this.$rootScope.$new();
|
||||
this.$scope.uiState = handlers.uiState;
|
||||
this.$scope.filter = handlers.event;
|
||||
updateScope();
|
||||
this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope));
|
||||
this.$scope.$apply();
|
||||
} else {
|
||||
updateScope();
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -124,16 +124,25 @@ export class VisLegend extends PureComponent<VisLegendProps, VisLegendState> {
|
|||
};
|
||||
|
||||
setFilterableLabels = (items: LegendItem[]): Promise<void> =>
|
||||
new Promise(async (resolve) => {
|
||||
const filterableLabels = new Set<string>();
|
||||
items.forEach(async (item) => {
|
||||
const canFilter = await this.canFilter(item);
|
||||
if (canFilter) {
|
||||
filterableLabels.add(item.label);
|
||||
}
|
||||
});
|
||||
new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const filterableLabels = new Set<string>();
|
||||
items.forEach(async (item) => {
|
||||
const canFilter = await this.canFilter(item);
|
||||
|
||||
this.setState({ filterableLabels }, resolve);
|
||||
if (canFilter) {
|
||||
filterableLabels.add(item.label);
|
||||
}
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
filterableLabels,
|
||||
},
|
||||
resolve
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
setLabels = (data: any, type: string) => {
|
||||
|
|
|
@ -22,13 +22,13 @@ export async function lazyLoadModules(): Promise<LazyLoadedModules> {
|
|||
return loadModulesPromise;
|
||||
}
|
||||
|
||||
loadModulesPromise = new Promise(async (resolve) => {
|
||||
const lazyImports = await import('./lazy');
|
||||
|
||||
resolve({
|
||||
...lazyImports,
|
||||
getHttp: () => getCoreStart().http,
|
||||
});
|
||||
loadModulesPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const lazyImports = await import('./lazy');
|
||||
resolve({ ...lazyImports, getHttp: () => getCoreStart().http });
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return loadModulesPromise;
|
||||
}
|
||||
|
|
|
@ -44,15 +44,18 @@ export async function lazyLoadModules(): Promise<LazyLoadedFileUploadModules> {
|
|||
return loadModulesPromise;
|
||||
}
|
||||
|
||||
loadModulesPromise = new Promise(async (resolve) => {
|
||||
const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy');
|
||||
|
||||
resolve({
|
||||
JsonUploadAndParse,
|
||||
importerFactory,
|
||||
getHttp,
|
||||
IndexNameForm,
|
||||
});
|
||||
loadModulesPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy');
|
||||
resolve({
|
||||
JsonUploadAndParse,
|
||||
importerFactory,
|
||||
getHttp,
|
||||
IndexNameForm,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return loadModulesPromise;
|
||||
}
|
||||
|
|
|
@ -107,17 +107,25 @@ export const useMappingsStateListener = ({ onChange, value, mappingsType }: Args
|
|||
validate: async () => {
|
||||
const configurationFormValidator =
|
||||
state.configuration.submitForm !== undefined
|
||||
? new Promise(async (resolve) => {
|
||||
const { isValid } = await state.configuration.submitForm!();
|
||||
resolve(isValid);
|
||||
? new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { isValid } = await state.configuration.submitForm!();
|
||||
resolve(isValid);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
: Promise.resolve(true);
|
||||
|
||||
const templatesFormValidator =
|
||||
state.templates.submitForm !== undefined
|
||||
? new Promise(async (resolve) => {
|
||||
const { isValid } = await state.templates.submitForm!();
|
||||
resolve(isValid);
|
||||
? new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { isValid } = await state.templates.submitForm!();
|
||||
resolve(isValid);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
: Promise.resolve(true);
|
||||
|
||||
|
|
|
@ -83,36 +83,39 @@ export async function lazyLoadMapModules(): Promise<LazyLoadedMapModules> {
|
|||
return loadModulesPromise;
|
||||
}
|
||||
|
||||
loadModulesPromise = new Promise(async (resolve) => {
|
||||
const {
|
||||
MapEmbeddable,
|
||||
getIndexPatternService,
|
||||
getMapsCapabilities,
|
||||
renderApp,
|
||||
createSecurityLayerDescriptors,
|
||||
registerLayerWizard,
|
||||
registerSource,
|
||||
createTileMapLayerDescriptor,
|
||||
createRegionMapLayerDescriptor,
|
||||
createBasemapLayerDescriptor,
|
||||
createESSearchSourceLayerDescriptor,
|
||||
suggestEMSTermJoinConfig,
|
||||
} = await import('./lazy');
|
||||
|
||||
resolve({
|
||||
MapEmbeddable,
|
||||
getIndexPatternService,
|
||||
getMapsCapabilities,
|
||||
renderApp,
|
||||
createSecurityLayerDescriptors,
|
||||
registerLayerWizard,
|
||||
registerSource,
|
||||
createTileMapLayerDescriptor,
|
||||
createRegionMapLayerDescriptor,
|
||||
createBasemapLayerDescriptor,
|
||||
createESSearchSourceLayerDescriptor,
|
||||
suggestEMSTermJoinConfig,
|
||||
});
|
||||
loadModulesPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
MapEmbeddable,
|
||||
getIndexPatternService,
|
||||
getMapsCapabilities,
|
||||
renderApp,
|
||||
createSecurityLayerDescriptors,
|
||||
registerLayerWizard,
|
||||
registerSource,
|
||||
createTileMapLayerDescriptor,
|
||||
createRegionMapLayerDescriptor,
|
||||
createBasemapLayerDescriptor,
|
||||
createESSearchSourceLayerDescriptor,
|
||||
suggestEMSTermJoinConfig,
|
||||
} = await import('./lazy');
|
||||
resolve({
|
||||
MapEmbeddable,
|
||||
getIndexPatternService,
|
||||
getMapsCapabilities,
|
||||
renderApp,
|
||||
createSecurityLayerDescriptors,
|
||||
registerLayerWizard,
|
||||
registerSource,
|
||||
createTileMapLayerDescriptor,
|
||||
createRegionMapLayerDescriptor,
|
||||
createBasemapLayerDescriptor,
|
||||
createESSearchSourceLayerDescriptor,
|
||||
suggestEMSTermJoinConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
return loadModulesPromise;
|
||||
}
|
||||
|
|
|
@ -23,27 +23,34 @@ export function loadNewJobCapabilities(
|
|||
jobType: JobType
|
||||
) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const serviceToUse =
|
||||
jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics;
|
||||
if (indexPatternId !== undefined) {
|
||||
// index pattern is being used
|
||||
const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId);
|
||||
await serviceToUse.initializeFromIndexPattern(indexPattern);
|
||||
resolve(serviceToUse.newJobCaps);
|
||||
} else if (savedSearchId !== undefined) {
|
||||
// saved search is being used
|
||||
// load the index pattern from the saved search
|
||||
const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId);
|
||||
if (indexPattern === null) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Cannot retrieve index pattern from saved search');
|
||||
try {
|
||||
const serviceToUse =
|
||||
jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics;
|
||||
|
||||
if (indexPatternId !== undefined) {
|
||||
// index pattern is being used
|
||||
const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId);
|
||||
await serviceToUse.initializeFromIndexPattern(indexPattern);
|
||||
resolve(serviceToUse.newJobCaps);
|
||||
} else if (savedSearchId !== undefined) {
|
||||
// saved search is being used
|
||||
// load the index pattern from the saved search
|
||||
const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId);
|
||||
|
||||
if (indexPattern === null) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Cannot retrieve index pattern from saved search');
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
await serviceToUse.initializeFromIndexPattern(indexPattern);
|
||||
resolve(serviceToUse.newJobCaps);
|
||||
} else {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
await serviceToUse.initializeFromIndexPattern(indexPattern);
|
||||
resolve(serviceToUse.newJobCaps);
|
||||
} else {
|
||||
reject();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,33 +25,34 @@ export async function resolveEmbeddableAnomalyChartsUserInput(
|
|||
const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
|
||||
|
||||
const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds);
|
||||
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
|
||||
const influencers = anomalyDetectorService.extractInfluencers(jobs);
|
||||
influencers.push(VIEW_BY_JOB_LABEL);
|
||||
|
||||
const modalSession = overlays.openModal(
|
||||
toMountPoint(
|
||||
<AnomalyChartsInitializer
|
||||
defaultTitle={title}
|
||||
initialInput={input}
|
||||
onCreate={({ panelTitle, maxSeriesToPlot }) => {
|
||||
modalSession.close();
|
||||
|
||||
resolve({
|
||||
jobIds,
|
||||
title: panelTitle,
|
||||
maxSeriesToPlot,
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
modalSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
try {
|
||||
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
|
||||
const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds);
|
||||
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
|
||||
const influencers = anomalyDetectorService.extractInfluencers(jobs);
|
||||
influencers.push(VIEW_BY_JOB_LABEL);
|
||||
const modalSession = overlays.openModal(
|
||||
toMountPoint(
|
||||
<AnomalyChartsInitializer
|
||||
defaultTitle={title}
|
||||
initialInput={input}
|
||||
onCreate={({ panelTitle, maxSeriesToPlot }) => {
|
||||
modalSession.close();
|
||||
resolve({
|
||||
jobIds,
|
||||
title: panelTitle,
|
||||
maxSeriesToPlot,
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
modalSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,31 +25,36 @@ export async function resolveAnomalySwimlaneUserInput(
|
|||
const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
|
||||
|
||||
const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds);
|
||||
|
||||
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
|
||||
|
||||
const influencers = anomalyDetectorService.extractInfluencers(jobs);
|
||||
influencers.push(VIEW_BY_JOB_LABEL);
|
||||
|
||||
const modalSession = overlays.openModal(
|
||||
toMountPoint(
|
||||
<AnomalySwimlaneInitializer
|
||||
defaultTitle={title}
|
||||
influencers={influencers}
|
||||
initialInput={input}
|
||||
onCreate={({ panelTitle, viewBy, swimlaneType }) => {
|
||||
modalSession.close();
|
||||
resolve({ jobIds, title: panelTitle, swimlaneType, viewBy });
|
||||
}}
|
||||
onCancel={() => {
|
||||
modalSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
try {
|
||||
const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds);
|
||||
const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds);
|
||||
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
|
||||
const influencers = anomalyDetectorService.extractInfluencers(jobs);
|
||||
influencers.push(VIEW_BY_JOB_LABEL);
|
||||
const modalSession = overlays.openModal(
|
||||
toMountPoint(
|
||||
<AnomalySwimlaneInitializer
|
||||
defaultTitle={title}
|
||||
influencers={influencers}
|
||||
initialInput={input}
|
||||
onCreate={({ panelTitle, viewBy, swimlaneType }) => {
|
||||
modalSession.close();
|
||||
resolve({
|
||||
jobIds,
|
||||
title: panelTitle,
|
||||
swimlaneType,
|
||||
viewBy,
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
modalSession.close();
|
||||
reject();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -38,56 +38,65 @@ export async function resolveJobSelection(
|
|||
} = coreStart;
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const maps = {
|
||||
groupsMap: getInitialGroupsMap([]),
|
||||
jobsMap: {},
|
||||
};
|
||||
try {
|
||||
const maps = {
|
||||
groupsMap: getInitialGroupsMap([]),
|
||||
jobsMap: {},
|
||||
};
|
||||
const tzConfig = uiSettings.get('dateFormat:tz');
|
||||
const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess();
|
||||
|
||||
const tzConfig = uiSettings.get('dateFormat:tz');
|
||||
const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess();
|
||||
|
||||
const onFlyoutClose = () => {
|
||||
flyoutSession.close();
|
||||
reject();
|
||||
};
|
||||
|
||||
const onSelectionConfirmed = async ({
|
||||
jobIds,
|
||||
groups,
|
||||
}: {
|
||||
jobIds: string[];
|
||||
groups: Array<{ groupId: string; jobIds: string[] }>;
|
||||
}) => {
|
||||
await flyoutSession.close();
|
||||
resolve({ jobIds, groups });
|
||||
};
|
||||
const flyoutSession = coreStart.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider services={{ ...coreStart, mlServices: getMlGlobalServices(http) }}>
|
||||
<JobSelectorFlyout
|
||||
selectedIds={selectedJobIds}
|
||||
withTimeRangeSelector={false}
|
||||
dateFormatTz={dateFormatTz}
|
||||
singleSelection={false}
|
||||
timeseriesOnly={true}
|
||||
onFlyoutClose={onFlyoutClose}
|
||||
onSelectionConfirmed={onSelectionConfirmed}
|
||||
maps={maps}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
),
|
||||
{
|
||||
'data-test-subj': 'mlFlyoutJobSelector',
|
||||
ownFocus: true,
|
||||
closeButtonAriaLabel: 'jobSelectorFlyout',
|
||||
}
|
||||
);
|
||||
|
||||
// Close the flyout when user navigates out of the dashboard plugin
|
||||
currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => {
|
||||
if (appId !== DashboardConstants.DASHBOARDS_ID) {
|
||||
const onFlyoutClose = () => {
|
||||
flyoutSession.close();
|
||||
}
|
||||
});
|
||||
reject();
|
||||
};
|
||||
|
||||
const onSelectionConfirmed = async ({
|
||||
jobIds,
|
||||
groups,
|
||||
}: {
|
||||
jobIds: string[];
|
||||
groups: Array<{
|
||||
groupId: string;
|
||||
jobIds: string[];
|
||||
}>;
|
||||
}) => {
|
||||
await flyoutSession.close();
|
||||
resolve({
|
||||
jobIds,
|
||||
groups,
|
||||
});
|
||||
};
|
||||
|
||||
const flyoutSession = coreStart.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider services={{ ...coreStart, mlServices: getMlGlobalServices(http) }}>
|
||||
<JobSelectorFlyout
|
||||
selectedIds={selectedJobIds}
|
||||
withTimeRangeSelector={false}
|
||||
dateFormatTz={dateFormatTz}
|
||||
singleSelection={false}
|
||||
timeseriesOnly={true}
|
||||
onFlyoutClose={onFlyoutClose}
|
||||
onSelectionConfirmed={onSelectionConfirmed}
|
||||
maps={maps}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
),
|
||||
{
|
||||
'data-test-subj': 'mlFlyoutJobSelector',
|
||||
ownFocus: true,
|
||||
closeButtonAriaLabel: 'jobSelectorFlyout',
|
||||
}
|
||||
); // Close the flyout when user navigates out of the dashboard plugin
|
||||
|
||||
currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => {
|
||||
if (appId !== DashboardConstants.DASHBOARDS_ID) {
|
||||
flyoutSession.close();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -98,27 +98,39 @@ async function getPaginatedThroughputData(pipelines, req, lsIndexPattern, throug
|
|||
const metricSeriesData = Object.values(
|
||||
await Promise.all(
|
||||
pipelines.map((pipeline) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const data = await getMetrics(
|
||||
req,
|
||||
lsIndexPattern,
|
||||
[throughputMetric],
|
||||
[
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { type: 'logstash_stats' } },
|
||||
{ term: { 'metricset.name': 'stats' } },
|
||||
],
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const data = await getMetrics(
|
||||
req,
|
||||
lsIndexPattern,
|
||||
[throughputMetric],
|
||||
[
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
type: 'logstash_stats',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'metricset.name': 'stats',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
pipeline,
|
||||
},
|
||||
],
|
||||
{
|
||||
pipeline,
|
||||
},
|
||||
2
|
||||
);
|
||||
resolve(reduceData(pipeline, data));
|
||||
2
|
||||
);
|
||||
resolve(reduceData(pipeline, data));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
)
|
||||
|
@ -184,27 +196,38 @@ async function getPipelines(req, lsIndexPattern, pipelines, throughputMetric, no
|
|||
async function getThroughputPipelines(req, lsIndexPattern, pipelines, throughputMetric) {
|
||||
const metricsResponse = await Promise.all(
|
||||
pipelines.map((pipeline) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const data = await getMetrics(
|
||||
req,
|
||||
lsIndexPattern,
|
||||
[throughputMetric],
|
||||
[
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { type: 'logstash_stats' } },
|
||||
{ term: { 'metricset.name': 'stats' } },
|
||||
],
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const data = await getMetrics(
|
||||
req,
|
||||
lsIndexPattern,
|
||||
[throughputMetric],
|
||||
[
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
type: 'logstash_stats',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'metricset.name': 'stats',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
pipeline,
|
||||
}
|
||||
);
|
||||
|
||||
resolve(reduceData(pipeline, data));
|
||||
],
|
||||
{
|
||||
pipeline,
|
||||
}
|
||||
);
|
||||
resolve(reduceData(pipeline, data));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -317,69 +317,73 @@ export class SessionIndex {
|
|||
|
||||
const sessionIndexTemplateName = `${this.options.kibanaIndexName}_security_session_index_template_${SESSION_INDEX_TEMPLATE_VERSION}`;
|
||||
return (this.indexInitialization = new Promise<void>(async (resolve, reject) => {
|
||||
// Check if required index template exists.
|
||||
let indexTemplateExists = false;
|
||||
try {
|
||||
indexTemplateExists = (
|
||||
await this.options.elasticsearchClient.indices.existsTemplate({
|
||||
name: sessionIndexTemplateName,
|
||||
})
|
||||
).body;
|
||||
} catch (err) {
|
||||
this.options.logger.error(
|
||||
`Failed to check if session index template exists: ${err.message}`
|
||||
);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
// Create index template if it doesn't exist.
|
||||
if (indexTemplateExists) {
|
||||
this.options.logger.debug('Session index template already exists.');
|
||||
} else {
|
||||
// Check if required index template exists.
|
||||
let indexTemplateExists = false;
|
||||
try {
|
||||
await this.options.elasticsearchClient.indices.putTemplate({
|
||||
name: sessionIndexTemplateName,
|
||||
body: getSessionIndexTemplate(this.indexName),
|
||||
});
|
||||
this.options.logger.debug('Successfully created session index template.');
|
||||
indexTemplateExists = (
|
||||
await this.options.elasticsearchClient.indices.existsTemplate({
|
||||
name: sessionIndexTemplateName,
|
||||
})
|
||||
).body;
|
||||
} catch (err) {
|
||||
this.options.logger.error(`Failed to create session index template: ${err.message}`);
|
||||
this.options.logger.error(
|
||||
`Failed to check if session index template exists: ${err.message}`
|
||||
);
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if required index exists. We cannot be sure that automatic creation of indices is
|
||||
// always enabled, so we create session index explicitly.
|
||||
let indexExists = false;
|
||||
try {
|
||||
indexExists = (
|
||||
await this.options.elasticsearchClient.indices.exists({ index: this.indexName })
|
||||
).body;
|
||||
} catch (err) {
|
||||
this.options.logger.error(`Failed to check if session index exists: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
// Create index if it doesn't exist.
|
||||
if (indexExists) {
|
||||
this.options.logger.debug('Session index already exists.');
|
||||
} else {
|
||||
try {
|
||||
await this.options.elasticsearchClient.indices.create({ index: this.indexName });
|
||||
this.options.logger.debug('Successfully created session index.');
|
||||
} catch (err) {
|
||||
// There can be a race condition if index is created by another Kibana instance.
|
||||
if (err?.body?.error?.type === 'resource_already_exists_exception') {
|
||||
this.options.logger.debug('Session index already exists.');
|
||||
} else {
|
||||
this.options.logger.error(`Failed to create session index: ${err.message}`);
|
||||
// Create index template if it doesn't exist.
|
||||
if (indexTemplateExists) {
|
||||
this.options.logger.debug('Session index template already exists.');
|
||||
} else {
|
||||
try {
|
||||
await this.options.elasticsearchClient.indices.putTemplate({
|
||||
name: sessionIndexTemplateName,
|
||||
body: getSessionIndexTemplate(this.indexName),
|
||||
});
|
||||
this.options.logger.debug('Successfully created session index template.');
|
||||
} catch (err) {
|
||||
this.options.logger.error(`Failed to create session index template: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify any consumers that are awaiting on this promise and immediately reset it.
|
||||
resolve();
|
||||
// Check if required index exists. We cannot be sure that automatic creation of indices is
|
||||
// always enabled, so we create session index explicitly.
|
||||
let indexExists = false;
|
||||
try {
|
||||
indexExists = (
|
||||
await this.options.elasticsearchClient.indices.exists({ index: this.indexName })
|
||||
).body;
|
||||
} catch (err) {
|
||||
this.options.logger.error(`Failed to check if session index exists: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
// Create index if it doesn't exist.
|
||||
if (indexExists) {
|
||||
this.options.logger.debug('Session index already exists.');
|
||||
} else {
|
||||
try {
|
||||
await this.options.elasticsearchClient.indices.create({ index: this.indexName });
|
||||
this.options.logger.debug('Successfully created session index.');
|
||||
} catch (err) {
|
||||
// There can be a race condition if index is created by another Kibana instance.
|
||||
if (err?.body?.error?.type === 'resource_already_exists_exception') {
|
||||
this.options.logger.debug('Session index already exists.');
|
||||
} else {
|
||||
this.options.logger.error(`Failed to create session index: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify any consumers that are awaiting on this promise and immediately reset it.
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.indexInitialization = undefined;
|
||||
}));
|
||||
|
|
|
@ -128,208 +128,225 @@ export const importRulesRoute = (
|
|||
const batchParseObjects = chunkParseObjects.shift() ?? [];
|
||||
const newImportRuleResponse = await Promise.all(
|
||||
batchParseObjects.reduce<Array<Promise<ImportRuleResponse>>>((accum, parsedRule) => {
|
||||
const importsWorkerPromise = new Promise<ImportRuleResponse>(async (resolve) => {
|
||||
if (parsedRule instanceof Error) {
|
||||
// If the JSON object had a validation or parse error then we return
|
||||
// early with the error and an (unknown) for the ruleId
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
statusCode: 400,
|
||||
message: parsedRule.message,
|
||||
})
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
anomaly_threshold: anomalyThreshold,
|
||||
author,
|
||||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
event_category_override: eventCategoryOverride,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query: queryOrUndefined,
|
||||
language: languageOrUndefined,
|
||||
license,
|
||||
machine_learning_job_id: machineLearningJobId,
|
||||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
meta,
|
||||
filters: filtersRest,
|
||||
rule_id: ruleId,
|
||||
index,
|
||||
interval,
|
||||
max_signals: maxSignals,
|
||||
risk_score: riskScore,
|
||||
risk_score_mapping: riskScoreMapping,
|
||||
rule_name_override: ruleNameOverride,
|
||||
name,
|
||||
severity,
|
||||
severity_mapping: severityMapping,
|
||||
tags,
|
||||
threat,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
threat_indicator_path: threatIndicatorPath,
|
||||
concurrent_searches: concurrentSearches,
|
||||
items_per_search: itemsPerSearch,
|
||||
threshold,
|
||||
timestamp_override: timestampOverride,
|
||||
to,
|
||||
type,
|
||||
references,
|
||||
note,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
throttle,
|
||||
version,
|
||||
exceptions_list: exceptionsList,
|
||||
} = parsedRule;
|
||||
const importsWorkerPromise = new Promise<ImportRuleResponse>(
|
||||
async (resolve, reject) => {
|
||||
try {
|
||||
if (parsedRule instanceof Error) {
|
||||
// If the JSON object had a validation or parse error then we return
|
||||
// early with the error and an (unknown) for the ruleId
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
statusCode: 400,
|
||||
message: parsedRule.message,
|
||||
})
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
|
||||
|
||||
const language =
|
||||
!isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined;
|
||||
|
||||
// TODO: Fix these either with an is conversion or by better typing them within io-ts
|
||||
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
|
||||
|
||||
throwHttpError(await mlAuthz.validateRuleType(type));
|
||||
|
||||
const rule = await readRules({ rulesClient, ruleId, id: undefined });
|
||||
if (rule == null) {
|
||||
await createRules({
|
||||
rulesClient,
|
||||
anomalyThreshold,
|
||||
const {
|
||||
anomaly_threshold: anomalyThreshold,
|
||||
author,
|
||||
buildingBlockType,
|
||||
building_block_type: buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
eventCategoryOverride,
|
||||
falsePositives,
|
||||
event_category_override: eventCategoryOverride,
|
||||
false_positives: falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
query: queryOrUndefined,
|
||||
language: languageOrUndefined,
|
||||
license,
|
||||
machineLearningJobId,
|
||||
outputIndex: signalsIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
machine_learning_job_id: machineLearningJobId,
|
||||
output_index: outputIndex,
|
||||
saved_id: savedId,
|
||||
meta,
|
||||
filters,
|
||||
ruleId,
|
||||
filters: filtersRest,
|
||||
rule_id: ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
name,
|
||||
riskScore,
|
||||
riskScoreMapping,
|
||||
ruleNameOverride,
|
||||
severity,
|
||||
severityMapping,
|
||||
tags,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatIndicatorPath,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
version,
|
||||
exceptionsList,
|
||||
actions: [], // Actions are not imported nor exported at this time
|
||||
});
|
||||
resolve({ rule_id: ruleId, status_code: 200 });
|
||||
} else if (rule != null && request.query.overwrite) {
|
||||
await patchRules({
|
||||
rulesClient,
|
||||
author,
|
||||
buildingBlockType,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
ruleStatusClient,
|
||||
description,
|
||||
enabled,
|
||||
eventCategoryOverride,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
license,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
riskScoreMapping,
|
||||
ruleNameOverride,
|
||||
max_signals: maxSignals,
|
||||
risk_score: riskScore,
|
||||
risk_score_mapping: riskScoreMapping,
|
||||
rule_name_override: ruleNameOverride,
|
||||
name,
|
||||
severity,
|
||||
severityMapping,
|
||||
severity_mapping: severityMapping,
|
||||
tags,
|
||||
timestampOverride,
|
||||
throttle,
|
||||
threat,
|
||||
threat_filters: threatFilters,
|
||||
threat_index: threatIndex,
|
||||
threat_query: threatQuery,
|
||||
threat_mapping: threatMapping,
|
||||
threat_language: threatLanguage,
|
||||
threat_indicator_path: threatIndicatorPath,
|
||||
concurrent_searches: concurrentSearches,
|
||||
items_per_search: itemsPerSearch,
|
||||
threshold,
|
||||
timestamp_override: timestampOverride,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
references,
|
||||
note,
|
||||
timeline_id: timelineId,
|
||||
timeline_title: timelineTitle,
|
||||
throttle,
|
||||
version,
|
||||
exceptionsList,
|
||||
anomalyThreshold,
|
||||
machineLearningJobId,
|
||||
actions: undefined,
|
||||
});
|
||||
resolve({ rule_id: ruleId, status_code: 200 });
|
||||
} else if (rule != null) {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
exceptions_list: exceptionsList,
|
||||
} = parsedRule;
|
||||
|
||||
try {
|
||||
const query =
|
||||
!isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined;
|
||||
const language =
|
||||
!isMlRule(type) && languageOrUndefined == null
|
||||
? 'kuery'
|
||||
: languageOrUndefined; // TODO: Fix these either with an is conversion or by better typing them within io-ts
|
||||
|
||||
const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[];
|
||||
throwHttpError(await mlAuthz.validateRuleType(type));
|
||||
const rule = await readRules({
|
||||
rulesClient,
|
||||
ruleId,
|
||||
statusCode: 409,
|
||||
message: `rule_id: "${ruleId}" already exists`,
|
||||
})
|
||||
);
|
||||
id: undefined,
|
||||
});
|
||||
|
||||
if (rule == null) {
|
||||
await createRules({
|
||||
rulesClient,
|
||||
anomalyThreshold,
|
||||
author,
|
||||
buildingBlockType,
|
||||
description,
|
||||
enabled,
|
||||
eventCategoryOverride,
|
||||
falsePositives,
|
||||
from,
|
||||
immutable,
|
||||
query,
|
||||
language,
|
||||
license,
|
||||
machineLearningJobId,
|
||||
outputIndex: signalsIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
ruleId,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
name,
|
||||
riskScore,
|
||||
riskScoreMapping,
|
||||
ruleNameOverride,
|
||||
severity,
|
||||
severityMapping,
|
||||
tags,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatIndicatorPath,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
timestampOverride,
|
||||
references,
|
||||
note,
|
||||
version,
|
||||
exceptionsList,
|
||||
actions: [], // Actions are not imported nor exported at this time
|
||||
});
|
||||
resolve({
|
||||
rule_id: ruleId,
|
||||
status_code: 200,
|
||||
});
|
||||
} else if (rule != null && request.query.overwrite) {
|
||||
await patchRules({
|
||||
rulesClient,
|
||||
author,
|
||||
buildingBlockType,
|
||||
spaceId: context.securitySolution.getSpaceId(),
|
||||
ruleStatusClient,
|
||||
description,
|
||||
enabled,
|
||||
eventCategoryOverride,
|
||||
falsePositives,
|
||||
from,
|
||||
query,
|
||||
language,
|
||||
license,
|
||||
outputIndex,
|
||||
savedId,
|
||||
timelineId,
|
||||
timelineTitle,
|
||||
meta,
|
||||
filters,
|
||||
rule,
|
||||
index,
|
||||
interval,
|
||||
maxSignals,
|
||||
riskScore,
|
||||
riskScoreMapping,
|
||||
ruleNameOverride,
|
||||
name,
|
||||
severity,
|
||||
severityMapping,
|
||||
tags,
|
||||
timestampOverride,
|
||||
throttle,
|
||||
to,
|
||||
type,
|
||||
threat,
|
||||
threshold,
|
||||
threatFilters,
|
||||
threatIndex,
|
||||
threatQuery,
|
||||
threatMapping,
|
||||
threatLanguage,
|
||||
concurrentSearches,
|
||||
itemsPerSearch,
|
||||
references,
|
||||
note,
|
||||
version,
|
||||
exceptionsList,
|
||||
anomalyThreshold,
|
||||
machineLearningJobId,
|
||||
actions: undefined,
|
||||
});
|
||||
resolve({
|
||||
rule_id: ruleId,
|
||||
status_code: 200,
|
||||
});
|
||||
} else if (rule != null) {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: 409,
|
||||
message: `rule_id: "${ruleId}" already exists`,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: err.statusCode ?? 400,
|
||||
message: err.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
} catch (err) {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
ruleId,
|
||||
statusCode: err.statusCode ?? 400,
|
||||
message: err.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
);
|
||||
return [...accum, importsWorkerPromise];
|
||||
}, [])
|
||||
);
|
||||
|
|
|
@ -106,133 +106,132 @@ export const importTimelines = async (
|
|||
batchParseObjects.reduce<Array<Promise<ImportTimelineResponse>>>((accum, parsedTimeline) => {
|
||||
const importsWorkerPromise = new Promise<ImportTimelineResponse>(
|
||||
async (resolve, reject) => {
|
||||
if (parsedTimeline instanceof Error) {
|
||||
// If the JSON object had a validation or parse error then we return
|
||||
// early with the error and an (unknown) for the ruleId
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
statusCode: 400,
|
||||
message: parsedTimeline.message,
|
||||
})
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
savedObjectId,
|
||||
pinnedEventIds,
|
||||
globalNotes,
|
||||
eventNotes,
|
||||
status,
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
title,
|
||||
timelineType,
|
||||
version,
|
||||
} = parsedTimeline;
|
||||
|
||||
const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline);
|
||||
let newTimeline = null;
|
||||
try {
|
||||
const compareTimelinesStatus = new CompareTimelinesStatus({
|
||||
status,
|
||||
timelineType,
|
||||
title,
|
||||
timelineInput: {
|
||||
id: savedObjectId,
|
||||
version,
|
||||
},
|
||||
templateTimelineInput: {
|
||||
id: templateTimelineId,
|
||||
version: templateTimelineVersion,
|
||||
},
|
||||
frameworkRequest,
|
||||
});
|
||||
await compareTimelinesStatus.init();
|
||||
const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline;
|
||||
if (compareTimelinesStatus.isCreatableViaImport) {
|
||||
// create timeline / timeline template
|
||||
newTimeline = await createTimelines({
|
||||
frameworkRequest,
|
||||
timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline),
|
||||
pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds,
|
||||
notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes],
|
||||
isImmutable,
|
||||
overrideNotesOwner: false,
|
||||
});
|
||||
|
||||
resolve({
|
||||
timeline_id: newTimeline.timeline.savedObjectId,
|
||||
status_code: 200,
|
||||
action: TimelineStatusActions.createViaImport,
|
||||
});
|
||||
}
|
||||
|
||||
if (!compareTimelinesStatus.isHandlingTemplateTimeline) {
|
||||
const errorMessage = compareTimelinesStatus.checkIsFailureCases(
|
||||
TimelineStatusActions.createViaImport
|
||||
);
|
||||
const message = errorMessage?.body ?? DEFAULT_ERROR;
|
||||
|
||||
if (parsedTimeline instanceof Error) {
|
||||
// If the JSON object had a validation or parse error then we return
|
||||
// early with the error and an (unknown) for the ruleId
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
id: savedObjectId ?? 'unknown',
|
||||
statusCode: 409,
|
||||
message,
|
||||
statusCode: 400,
|
||||
message: parsedTimeline.message,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (compareTimelinesStatus.isUpdatableViaImport) {
|
||||
// update timeline template
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
savedObjectId,
|
||||
pinnedEventIds,
|
||||
globalNotes,
|
||||
eventNotes,
|
||||
status,
|
||||
templateTimelineId,
|
||||
templateTimelineVersion,
|
||||
title,
|
||||
timelineType,
|
||||
version,
|
||||
} = parsedTimeline;
|
||||
const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline);
|
||||
let newTimeline = null;
|
||||
|
||||
try {
|
||||
const compareTimelinesStatus = new CompareTimelinesStatus({
|
||||
status,
|
||||
timelineType,
|
||||
title,
|
||||
timelineInput: {
|
||||
id: savedObjectId,
|
||||
version,
|
||||
},
|
||||
templateTimelineInput: {
|
||||
id: templateTimelineId,
|
||||
version: templateTimelineVersion,
|
||||
},
|
||||
frameworkRequest,
|
||||
});
|
||||
await compareTimelinesStatus.init();
|
||||
const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline;
|
||||
|
||||
if (compareTimelinesStatus.isCreatableViaImport) {
|
||||
// create timeline / timeline template
|
||||
newTimeline = await createTimelines({
|
||||
frameworkRequest,
|
||||
timeline: parsedTimelineObject,
|
||||
timelineSavedObjectId: compareTimelinesStatus.timelineId,
|
||||
timelineVersion: compareTimelinesStatus.timelineVersion,
|
||||
notes: globalNotes,
|
||||
existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds,
|
||||
timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline),
|
||||
pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds,
|
||||
notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes],
|
||||
isImmutable,
|
||||
overrideNotesOwner: false,
|
||||
});
|
||||
|
||||
resolve({
|
||||
timeline_id: newTimeline.timeline.savedObjectId,
|
||||
status_code: 200,
|
||||
action: TimelineStatusActions.updateViaImport,
|
||||
action: TimelineStatusActions.createViaImport,
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!compareTimelinesStatus.isHandlingTemplateTimeline) {
|
||||
const errorMessage = compareTimelinesStatus.checkIsFailureCases(
|
||||
TimelineStatusActions.updateViaImport
|
||||
TimelineStatusActions.createViaImport
|
||||
);
|
||||
|
||||
const message = errorMessage?.body ?? DEFAULT_ERROR;
|
||||
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
id:
|
||||
savedObjectId ??
|
||||
(templateTimelineId
|
||||
? `(template_timeline_id) ${templateTimelineId}`
|
||||
: 'unknown'),
|
||||
id: savedObjectId ?? 'unknown',
|
||||
statusCode: 409,
|
||||
message,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (compareTimelinesStatus.isUpdatableViaImport) {
|
||||
// update timeline template
|
||||
newTimeline = await createTimelines({
|
||||
frameworkRequest,
|
||||
timeline: parsedTimelineObject,
|
||||
timelineSavedObjectId: compareTimelinesStatus.timelineId,
|
||||
timelineVersion: compareTimelinesStatus.timelineVersion,
|
||||
notes: globalNotes,
|
||||
existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds,
|
||||
isImmutable,
|
||||
overrideNotesOwner: false,
|
||||
});
|
||||
resolve({
|
||||
timeline_id: newTimeline.timeline.savedObjectId,
|
||||
status_code: 200,
|
||||
action: TimelineStatusActions.updateViaImport,
|
||||
});
|
||||
} else {
|
||||
const errorMessage = compareTimelinesStatus.checkIsFailureCases(
|
||||
TimelineStatusActions.updateViaImport
|
||||
);
|
||||
const message = errorMessage?.body ?? DEFAULT_ERROR;
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
id:
|
||||
savedObjectId ??
|
||||
(templateTimelineId
|
||||
? `(template_timeline_id) ${templateTimelineId}`
|
||||
: 'unknown'),
|
||||
statusCode: 409,
|
||||
message,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
id:
|
||||
savedObjectId ??
|
||||
(templateTimelineId
|
||||
? `(template_timeline_id) ${templateTimelineId}`
|
||||
: 'unknown'),
|
||||
statusCode: 400,
|
||||
message: err.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
resolve(
|
||||
createBulkErrorObject({
|
||||
id:
|
||||
savedObjectId ??
|
||||
(templateTimelineId
|
||||
? `(template_timeline_id) ${templateTimelineId}`
|
||||
: 'unknown'),
|
||||
statusCode: 400,
|
||||
message: err.message,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -47,62 +47,62 @@ describe('Configuration Statistics Aggregator', () => {
|
|||
};
|
||||
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
createConfigurationAggregator(configuration, managedConfig)
|
||||
.pipe(take(3), bufferCount(3))
|
||||
.subscribe(([initial, updatedWorkers, updatedInterval]) => {
|
||||
expect(initial.value).toEqual({
|
||||
max_workers: 10,
|
||||
poll_interval: 6000000,
|
||||
max_poll_inactivity_cycles: 10,
|
||||
request_capacity: 1000,
|
||||
monitored_aggregated_stats_refresh_rate: 5000,
|
||||
monitored_stats_running_average_window: 50,
|
||||
monitored_task_execution_thresholds: {
|
||||
default: {
|
||||
error_threshold: 90,
|
||||
warn_threshold: 80,
|
||||
try {
|
||||
createConfigurationAggregator(configuration, managedConfig)
|
||||
.pipe(take(3), bufferCount(3))
|
||||
.subscribe(([initial, updatedWorkers, updatedInterval]) => {
|
||||
expect(initial.value).toEqual({
|
||||
max_workers: 10,
|
||||
poll_interval: 6000000,
|
||||
max_poll_inactivity_cycles: 10,
|
||||
request_capacity: 1000,
|
||||
monitored_aggregated_stats_refresh_rate: 5000,
|
||||
monitored_stats_running_average_window: 50,
|
||||
monitored_task_execution_thresholds: {
|
||||
default: {
|
||||
error_threshold: 90,
|
||||
warn_threshold: 80,
|
||||
},
|
||||
custom: {},
|
||||
},
|
||||
custom: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedWorkers.value).toEqual({
|
||||
max_workers: 8,
|
||||
poll_interval: 6000000,
|
||||
max_poll_inactivity_cycles: 10,
|
||||
request_capacity: 1000,
|
||||
monitored_aggregated_stats_refresh_rate: 5000,
|
||||
monitored_stats_running_average_window: 50,
|
||||
monitored_task_execution_thresholds: {
|
||||
default: {
|
||||
error_threshold: 90,
|
||||
warn_threshold: 80,
|
||||
});
|
||||
expect(updatedWorkers.value).toEqual({
|
||||
max_workers: 8,
|
||||
poll_interval: 6000000,
|
||||
max_poll_inactivity_cycles: 10,
|
||||
request_capacity: 1000,
|
||||
monitored_aggregated_stats_refresh_rate: 5000,
|
||||
monitored_stats_running_average_window: 50,
|
||||
monitored_task_execution_thresholds: {
|
||||
default: {
|
||||
error_threshold: 90,
|
||||
warn_threshold: 80,
|
||||
},
|
||||
custom: {},
|
||||
},
|
||||
custom: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedInterval.value).toEqual({
|
||||
max_workers: 8,
|
||||
poll_interval: 3000,
|
||||
max_poll_inactivity_cycles: 10,
|
||||
request_capacity: 1000,
|
||||
monitored_aggregated_stats_refresh_rate: 5000,
|
||||
monitored_stats_running_average_window: 50,
|
||||
monitored_task_execution_thresholds: {
|
||||
default: {
|
||||
error_threshold: 90,
|
||||
warn_threshold: 80,
|
||||
});
|
||||
expect(updatedInterval.value).toEqual({
|
||||
max_workers: 8,
|
||||
poll_interval: 3000,
|
||||
max_poll_inactivity_cycles: 10,
|
||||
request_capacity: 1000,
|
||||
monitored_aggregated_stats_refresh_rate: 5000,
|
||||
monitored_stats_running_average_window: 50,
|
||||
monitored_task_execution_thresholds: {
|
||||
default: {
|
||||
error_threshold: 90,
|
||||
warn_threshold: 80,
|
||||
},
|
||||
custom: {},
|
||||
},
|
||||
custom: {},
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
}, reject);
|
||||
|
||||
managedConfig.maxWorkersConfiguration$.next(8);
|
||||
|
||||
managedConfig.pollIntervalConfiguration$.next(3000);
|
||||
});
|
||||
resolve();
|
||||
}, reject);
|
||||
managedConfig.maxWorkersConfiguration$.next(8);
|
||||
managedConfig.pollIntervalConfiguration$.next(3000);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -328,27 +328,44 @@ describe('Workload Statistics Aggregator', () => {
|
|||
loggingSystemMock.create().get()
|
||||
);
|
||||
|
||||
return new Promise<void>(async (resolve) => {
|
||||
workloadAggregator.pipe(first()).subscribe((result) => {
|
||||
expect(result.key).toEqual('workload');
|
||||
expect(result.value).toMatchObject({
|
||||
count: 4,
|
||||
task_types: {
|
||||
actions_telemetry: { count: 2, status: { idle: 2 } },
|
||||
alerting_telemetry: { count: 1, status: { idle: 1 } },
|
||||
session_cleanup: { count: 1, status: { idle: 1 } },
|
||||
},
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
workloadAggregator.pipe(first()).subscribe((result) => {
|
||||
expect(result.key).toEqual('workload');
|
||||
expect(result.value).toMatchObject({
|
||||
count: 4,
|
||||
task_types: {
|
||||
actions_telemetry: {
|
||||
count: 2,
|
||||
status: {
|
||||
idle: 2,
|
||||
},
|
||||
},
|
||||
alerting_telemetry: {
|
||||
count: 1,
|
||||
status: {
|
||||
idle: 1,
|
||||
},
|
||||
},
|
||||
session_cleanup: {
|
||||
count: 1,
|
||||
status: {
|
||||
idle: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
||||
availability$.next(false);
|
||||
|
||||
await sleep(10);
|
||||
expect(taskStore.aggregate).not.toHaveBeenCalled();
|
||||
await sleep(10);
|
||||
expect(taskStore.aggregate).not.toHaveBeenCalled();
|
||||
availability$.next(true);
|
||||
availability$.next(false);
|
||||
await sleep(10);
|
||||
expect(taskStore.aggregate).not.toHaveBeenCalled();
|
||||
await sleep(10);
|
||||
expect(taskStore.aggregate).not.toHaveBeenCalled();
|
||||
availability$.next(true);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -113,11 +113,18 @@ export class TaskScheduling {
|
|||
*/
|
||||
public async runNow(taskId: string): Promise<RunNowResult> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.awaitTaskRunResult(taskId)
|
||||
// don't expose state on runNow
|
||||
.then(({ id }) => resolve({ id }))
|
||||
.catch(reject);
|
||||
this.taskPollingLifecycle.attemptToRun(taskId);
|
||||
try {
|
||||
this.awaitTaskRunResult(taskId) // don't expose state on runNow
|
||||
.then(({ id }) =>
|
||||
resolve({
|
||||
id,
|
||||
})
|
||||
)
|
||||
.catch(reject);
|
||||
this.taskPollingLifecycle.attemptToRun(taskId);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -137,39 +144,42 @@ export class TaskScheduling {
|
|||
taskInstance: task,
|
||||
});
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves.
|
||||
// However, we do not wait to await this promise, as we want later execution to happen in parallel.
|
||||
// The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id).
|
||||
// However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue).
|
||||
|
||||
// The reason for all this confusion? Timing.
|
||||
|
||||
// In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything.
|
||||
// If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility.
|
||||
const { cancel, resolveOnCancel } = cancellablePromise();
|
||||
this.awaitTaskRunResult(id, resolveOnCancel)
|
||||
.then((arg: RunNowResult) => {
|
||||
resolve(arg);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
reject(err);
|
||||
try {
|
||||
// The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves.
|
||||
// However, we do not wait to await this promise, as we want later execution to happen in parallel.
|
||||
// The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id).
|
||||
// However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue).
|
||||
// The reason for all this confusion? Timing.
|
||||
// In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything.
|
||||
// If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility.
|
||||
const { cancel, resolveOnCancel } = cancellablePromise();
|
||||
this.awaitTaskRunResult(id, resolveOnCancel)
|
||||
.then((arg: RunNowResult) => {
|
||||
resolve(arg);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({
|
||||
id,
|
||||
scheduledAt: new Date(),
|
||||
runAt: new Date(),
|
||||
status: TaskStatus.Idle,
|
||||
ownerId: this.taskManagerId,
|
||||
...modifiedTask,
|
||||
});
|
||||
const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({
|
||||
id,
|
||||
scheduledAt: new Date(),
|
||||
runAt: new Date(),
|
||||
status: TaskStatus.Idle,
|
||||
ownerId: this.taskManagerId,
|
||||
...modifiedTask,
|
||||
});
|
||||
if (isErr(attemptToRunResult)) {
|
||||
cancel();
|
||||
reject(
|
||||
new EphemeralTaskRejectedDueToCapacityError(
|
||||
`Ephemeral Task of type ${task.taskType} was rejected`,
|
||||
task
|
||||
)
|
||||
);
|
||||
|
||||
if (isErr(attemptToRunResult)) {
|
||||
cancel();
|
||||
reject(
|
||||
new EphemeralTaskRejectedDueToCapacityError(
|
||||
`Ephemeral Task of type ${task.taskType} was rejected`,
|
||||
task
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -105,14 +105,20 @@ const search = async (engineName: string): Promise<ISearchResponse> => {
|
|||
// Since the App Search API does not issue document receipts, the only way to tell whether or not documents
|
||||
// are fully indexed is to poll the search endpoint.
|
||||
export const waitForIndexedDocs = (engineName: string) => {
|
||||
return new Promise<void>(async function (resolve) {
|
||||
let isReady = false;
|
||||
while (!isReady) {
|
||||
const response = await search(engineName);
|
||||
if (response.results && response.results.length > 0) {
|
||||
isReady = true;
|
||||
resolve();
|
||||
return new Promise<void>(async function (resolve, reject) {
|
||||
try {
|
||||
let isReady = false;
|
||||
|
||||
while (!isReady) {
|
||||
const response = await search(engineName);
|
||||
|
||||
if (response.results && response.results.length > 0) {
|
||||
isReady = true;
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -116,21 +116,29 @@ export const waitFor = async (
|
|||
timeoutWait: number = 10
|
||||
) => {
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
let found = false;
|
||||
let numberOfTries = 0;
|
||||
while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) {
|
||||
const itPasses = await functionToTest();
|
||||
if (itPasses) {
|
||||
found = true;
|
||||
} else {
|
||||
numberOfTries++;
|
||||
try {
|
||||
let found = false;
|
||||
let numberOfTries = 0;
|
||||
|
||||
while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) {
|
||||
const itPasses = await functionToTest();
|
||||
|
||||
if (itPasses) {
|
||||
found = true;
|
||||
} else {
|
||||
numberOfTries++;
|
||||
}
|
||||
|
||||
await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait));
|
||||
}
|
||||
await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait));
|
||||
}
|
||||
if (found) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`timed out waiting for function ${functionName} condition to be true`));
|
||||
|
||||
if (found) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`timed out waiting for function ${functionName} condition to be true`));
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -6457,6 +6457,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa"
|
||||
integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w==
|
||||
|
||||
"@typescript-eslint/types@4.28.3":
|
||||
version "4.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7"
|
||||
integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA==
|
||||
|
||||
"@typescript-eslint/types@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf"
|
||||
|
@ -6490,6 +6495,19 @@
|
|||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@typescript-eslint/typescript-estree@^4.14.1":
|
||||
version "4.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb"
|
||||
integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.3"
|
||||
"@typescript-eslint/visitor-keys" "4.28.3"
|
||||
debug "^4.3.1"
|
||||
globby "^11.0.3"
|
||||
is-glob "^4.0.1"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.14.1":
|
||||
version "4.14.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91"
|
||||
|
@ -6498,6 +6516,14 @@
|
|||
"@typescript-eslint/types" "4.14.1"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.28.3":
|
||||
version "4.28.3"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4"
|
||||
integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.3"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0"
|
||||
|
@ -13120,6 +13146,11 @@ eslint-scope@^5.0.0:
|
|||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-traverse@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-traverse/-/eslint-traverse-1.0.0.tgz#108d360a171a6e6334e1af0cee905a93bd0dcc53"
|
||||
integrity sha512-bSp37rQs93LF8rZ409EI369DGCI4tELbFVmFNxI6QbuveS7VRxYVyUhwDafKN/enMyUh88HQQ7ZoGUHtPuGdcw==
|
||||
|
||||
eslint-utils@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
|
||||
|
@ -27535,6 +27566,13 @@ tsutils@^3.17.1:
|
|||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
|
|
Loading…
Reference in a new issue