From 1304c2106ee7ae76de6d43a7446b9ebc2befe5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 15 Oct 2020 15:35:13 +0200 Subject: [PATCH 01/79] [APM] Add missing ML privileges (#80553) --- x-pack/plugins/ml/common/types/capabilities.ts | 1 + .../test/apm_api_integration/common/authentication.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 42f056b890828..0d208dc0b1b28 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -9,6 +9,7 @@ import { PLUGIN_ID } from '../constants/app'; export const apmUserMlCapabilities = { canGetJobs: false, + canAccessML: false, }; export const userMlCapabilities = { diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 6179c88916639..501a844311334 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -29,6 +29,15 @@ const roles = { ], }, [ApmUser.apmReadUserWithoutMlAccess]: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, kibana: [ { base: [], @@ -74,7 +83,7 @@ const users = { roles: ['apm_user', ApmUser.apmReadUser], }, [ApmUser.apmReadUserWithoutMlAccess]: { - roles: ['apm_user', ApmUser.apmReadUserWithoutMlAccess], + roles: [ApmUser.apmReadUserWithoutMlAccess], }, [ApmUser.apmWriteUser]: { roles: ['apm_user', ApmUser.apmWriteUser], From c55863cf01b0a039fa6ac3461f542ab396488428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 15 Oct 2020 14:46:19 +0100 Subject: [PATCH 02/79] [APM] Update User Experience app callout code to reflect new name (#80641) --- .../components/app/TransactionOverview/index.tsx | 4 ++-- ...toringCallout.tsx => user_experience_callout.tsx} | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) rename x-pack/plugins/apm/public/components/app/TransactionOverview/{ClientSideMonitoringCallout.tsx => user_experience_callout.tsx} (75%) diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 7c887da6dc5e6..8c7d088d36eb2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -36,7 +36,7 @@ import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTy import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { ClientSideMonitoringCallout } from './ClientSideMonitoringCallout'; +import { UserExperienceCallout } from './user_experience_callout'; function getRedirectLocation({ urlParams, @@ -129,7 +129,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { {transactionType === TRANSACTION_PAGE_LOAD && ( <> - + )} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/user_experience_callout.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/user_experience_callout.tsx index becae4d7eb5d7..41e84d4acfba5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/user_experience_callout.tsx @@ -9,21 +9,21 @@ import { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -export function ClientSideMonitoringCallout() { +export function UserExperienceCallout() { const { core } = useApmPluginContext(); - const clientSideMonitoringHref = core.http.basePath.prepend(`/app/ux`); + const userExperienceHref = core.http.basePath.prepend(`/app/ux`); return ( {i18n.translate( - 'xpack.apm.transactionOverview.clientSideMonitoring.calloutText', + 'xpack.apm.transactionOverview.userExperience.calloutText', { defaultMessage: 'We are beyond excited to introduce a new experience for analyzing the user experience metrics specifically for your RUM services. It provides insights into the core vitals and visitor breakdown by browser and location. The app is always available in the left sidebar among the other Observability views.', @@ -31,9 +31,9 @@ export function ClientSideMonitoringCallout() { )} - + {i18n.translate( - 'xpack.apm.transactionOverview.clientSideMonitoring.linkLabel', + 'xpack.apm.transactionOverview.userExperience.linkLabel', { defaultMessage: 'Take me there' } )} From a8b34561e46b50c3f2132cda18649b8ffc9a1530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 15 Oct 2020 14:46:43 +0100 Subject: [PATCH 03/79] [Observability] Kibana home page Observability link pointing to `/landing` (#80636) --- x-pack/plugins/observability/public/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 2f83576f9dc5a..765cce0baaa1a 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -81,7 +81,7 @@ export class Plugin implements PluginClass Date: Thu, 15 Oct 2020 08:08:59 -0600 Subject: [PATCH 04/79] Update Security Solution Bug Report Template (#80668) --- .github/ISSUE_TEMPLATE/security_solution_bug_report.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md index 86d2b1405d4eb..0c24eb2f973f5 100644 --- a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -1,7 +1,8 @@ --- name: Security Solution Bug Report -about: Things break. Help us identify those things so we can fix them! +about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! title: '[Security Solution]' +labels: Team:Security Solution --- **Describe the bug:** From 40d6d4dd7d66f79191079fe190e84ad8963e7e25 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 15 Oct 2020 10:09:26 -0400 Subject: [PATCH 05/79] [Resolver] Fix flaky test (#80576) Under stress, this test would fail to finish in 5 seconds. With this new implementation it has better performance. --- .../test_utilities/simulator/index.tsx | 47 ++++++++++-------- .../resolver/view/clickthrough.test.tsx | 48 ++++++++++--------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index ea603f2583431..2a399b6844bd7 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -45,6 +45,23 @@ export class Simulator { */ private readonly sideEffectSimulator: SideEffectSimulator; + /** + * An `enzyme` supported CSS selector for process node elements. + */ + public static nodeElementSelector({ + entityID, + selected = false, + }: ProcessNodeElementSelectorOptions = {}): string { + let selector: string = baseNodeElementSelector; + if (entityID !== undefined) { + selector += `[data-test-resolver-node-id="${entityID}"]`; + } + if (selected) { + selector += '[aria-selected="true"]'; + } + return selector; + } + constructor({ dataAccessLayer, resolverComponentInstanceID, @@ -193,7 +210,7 @@ export class Simulator { * returns a `ReactWrapper` even if nothing is found, as that is how `enzyme` does things. */ public processNodeElements(options: ProcessNodeElementSelectorOptions = {}): ReactWrapper { - return this.domNodes(processNodeElementSelector(options)); + return this.domNodes(Simulator.nodeElementSelector(options)); } /** @@ -230,7 +247,7 @@ export class Simulator { */ public unselectedProcessNode(entityID: string): ReactWrapper { return this.processNodeElements({ entityID }).not( - processNodeElementSelector({ entityID, selected: true }) + Simulator.nodeElementSelector({ entityID, selected: true }) ); } @@ -265,6 +282,13 @@ export class Simulator { return this.resolveWrapper(() => this.domNodes(`[data-test-subj="${selector}"]`)); } + /** + * Given a `role`, return DOM nodes that have it. Use this to assert that ARIA roles are present as expected. + */ + public domNodesWithRole(role: string): ReactWrapper { + return this.domNodes(`[role="${role}"]`); + } + /** * Given a 'data-test-subj' selector, it will return the domNode */ @@ -318,7 +342,7 @@ export class Simulator { } } -const baseResolverSelector = '[data-test-subj="resolver:node"]'; +const baseNodeElementSelector = '[data-test-subj="resolver:node"]'; interface ProcessNodeElementSelectorOptions { /** @@ -330,20 +354,3 @@ interface ProcessNodeElementSelectorOptions { */ selected?: boolean; } - -/** - * An `enzyme` supported CSS selector for process node elements. - */ -function processNodeElementSelector({ - entityID, - selected = false, -}: ProcessNodeElementSelectorOptions = {}): string { - let selector: string = baseResolverSelector; - if (entityID !== undefined) { - selector += `[data-test-resolver-node-id="${entityID}"]`; - } - if (selected) { - selector += '[aria-selected="true"]'; - } - return selector; -} diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index dba1136193ee1..c781832dc8a3b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -137,28 +137,32 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', it('should render 3 elements with "treeitem" roles, each owned by an element with a "tree" role', async () => { await expect( - simulator.map(() => ({ - nodesOwnedByTrees: simulator.testSubject('resolver:node').filterWhere((domNode) => { - /** - * This test verifies corectness w.r.t. the tree/treeitem roles - * From W3C: `Authors MUST ensure elements with role treeitem are contained in, or owned by, an element with the role group or tree.` - * - * https://www.w3.org/TR/wai-aria-1.1/#tree - * https://www.w3.org/TR/wai-aria-1.1/#treeitem - * - * w3c defines two ways for an element to be an "owned element" - * 1. Any DOM descendant - * 2. Any element specified as a child via aria-owns - * (see: https://www.w3.org/TR/wai-aria-1.1/#dfn-owned-element) - * - * In the context of Resolver (as of this writing) nodes/treeitems are children of the tree, - * but they could be moved out of the tree, provided that the tree is given an `aria-owns` - * attribute referring to them (method 2 above). - */ - return domNode.closest('[role="tree"]').length === 1; - }).length, - })) - ).toYieldEqualTo({ nodesOwnedByTrees: 3 }); + simulator.map(() => { + /** + * This test verifies corectness w.r.t. the tree/treeitem roles + * From W3C: `Authors MUST ensure elements with role treeitem are contained in, or owned by, an element with the role group or tree.` + * + * https://www.w3.org/TR/wai-aria-1.1/#tree + * https://www.w3.org/TR/wai-aria-1.1/#treeitem + * + * w3c defines two ways for an element to be an "owned element" + * 1. Any DOM descendant + * 2. Any element specified as a child via aria-owns + * (see: https://www.w3.org/TR/wai-aria-1.1/#dfn-owned-element) + * + * In the context of Resolver (as of this writing) nodes/treeitems are children of the tree, + * but they could be moved out of the tree, provided that the tree is given an `aria-owns` + * attribute referring to them (method 2 above). + */ + const tree = simulator.domNodesWithRole('tree'); + return { + // There should be only one tree. + treeCount: tree.length, + // The tree should have 3 nodes in it. + nodesOwnedByTrees: tree.find(Simulator.nodeElementSelector()).length, + }; + }) + ).toYieldEqualTo({ treeCount: 1, nodesOwnedByTrees: 3 }); }); it(`should show links to the 3 nodes (with icons) in the node list.`, async () => { From ec692a7fa95e18b3823aa1497fa0cd0715fcd045 Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Thu, 15 Oct 2020 16:40:41 +0200 Subject: [PATCH 06/79] Fixed the problem with responsiveness of item details card. (#80645) * Fixed the problem with responsiveness of item details card. * Updated snapshots. * Updated snapshots after pulling master. --- .../__snapshots__/index.test.tsx.snap | 283 +- .../item_details_card/index.stories.tsx | 2 +- .../components/item_details_card/index.tsx | 64 +- .../__snapshots__/index.test.tsx.snap | 12428 ++++++++-------- .../__snapshots__/index.test.tsx.snap | 407 +- 5 files changed, 6387 insertions(+), 6797 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index 4bd2cd05d49d0..8014431192170 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -1,22 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`item_details_card ItemDetailsAction should render correctly 1`] = ` - - + - - primary - - - + primary + + `; exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = ` @@ -24,103 +23,98 @@ exports[`item_details_card ItemDetailsCard should render correctly with actions paddingSize="none" > - + + + + + + + + - - - - - - - - + some text + + some node + + + + - some text - - some node - + primary + + + secondary + + + - - - - primary - - - - - secondary - - - - - danger - - - + danger + - + - + `; @@ -130,66 +124,61 @@ exports[`item_details_card ItemDetailsCard should render correctly with no actio paddingSize="none" > - + + + + + + + + - - - - - - - - + some text + + some node + + + + - - some text - - some node - - - - - - - + gutterSize="s" + justifyContent="flexEnd" + /> + - + `; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx index e9d1825658bee..74f31a623969c 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx @@ -21,7 +21,7 @@ storiesOf('Components/ItemDetailsCard', module).add('default', () => { - {'content text'} + {'content text '} {'content node'} diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index c41c5f89c0068..37003961d67d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -81,11 +81,17 @@ ItemDetailsPropertySummary.displayName = 'ItemPropertySummary'; export const ItemDetailsAction: FC> = memo( ({ children, ...rest }) => ( - - - {children} - - + <> + + {children} + + ) ); @@ -99,32 +105,30 @@ export const ItemDetailsCard: FC = memo(({ children }) => { return ( - - - - - - {childElements.get(ItemDetailsPropertySummary)} - - - - - {childElements.get(OTHER_NODES)} - {childElements.has(ItemDetailsAction) && ( - - - {childElements.get(ItemDetailsAction)?.map((action, index) => ( - - {action} - - ))} - - - )} - - + + + + {childElements.get(ItemDetailsPropertySummary)} + + + + + +
{childElements.get(OTHER_NODES)}
+
+ {childElements.has(ItemDetailsAction) && ( + + + {childElements.get(ItemDetailsAction)?.map((action, index) => ( + + {action} + + ))} + + + )}
-
+
); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index dc549a5b4f2d2..4276debbac3af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -124,255 +124,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -388,255 +375,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 1 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 1 - - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -652,255 +626,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 2 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 2 - - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -916,255 +877,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 3 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 3 - - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1180,255 +1128,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 4 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 4 - - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1444,255 +1379,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 5 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 5 - - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
-
-
+
+
+
+
+ -
-
-
-
+ Remove + + +
@@ -1708,255 +1630,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 6 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 6 - - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1972,255 +1881,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 7 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 7 - - -
-
-
-
+ trusted app 7 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2236,255 +2132,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 8 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 8 - - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2500,255 +2383,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 9 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 9 - - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
-
-
-
-
-
- -
-
-
-
+
+
+
+
+
+
@@ -3054,255 +2924,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3318,255 +3175,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 1 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 1 - - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3582,255 +3426,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 2 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 2 - - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3846,255 +3677,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 3 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 3 - - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4110,255 +3928,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 4 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 4 - - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4374,255 +4179,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 5 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 5 - - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4638,255 +4430,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 6 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 6 - - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4902,255 +4681,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 7 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 7 - - -
-
-
-
+ trusted app 7 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5166,255 +4932,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 8 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 8 - - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5430,255 +5183,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 9 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 9 - - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5942,255 +5682,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6206,255 +5933,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 1 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 1 - - -
-
-
+ + trusted app 1 + + + +
+ OS +
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
-
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6470,255 +6184,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 2 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 2 - - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6734,255 +6435,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 3 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 3 - - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6998,255 +6686,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 4 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 4 - - -
-
-
-
+ trusted app 4 + + + +
+ OS +
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
-
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7262,255 +6937,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 5 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 5 - - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7526,519 +7188,493 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 6 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 6 - - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Name +
+
+ + + trusted app 7 + + +
+
+ OS +
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+
+
-
-
- Name -
-
- - - trusted app 7 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 7 - - -
-
-
-
-
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -8054,255 +7690,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 8 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 8 - - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -8318,255 +7941,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 9 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 9 - - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 181b59c65a3d5..5f652b39ffd56 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -779,252 +779,239 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
From 8940091cf9e31dd6e0b612d2be4b2ef402d2ea9f Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 15 Oct 2020 08:07:17 -0700 Subject: [PATCH 07/79] [ci-stats] record async chunk count (#80606) Co-authored-by: spalger --- packages/kbn-optimizer/src/optimizer/get_output_stats.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts index cc4cd05f42c3f..82d1a276ccdad 100644 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts @@ -109,6 +109,11 @@ export function getMetrics(log: ToolingLog, config: OptimizerConfig) { id: bundle.id, value: sumSize(asyncChunks), }, + { + group: `async chunk count`, + id: bundle.id, + value: asyncChunks.length, + }, { group: `miscellaneous assets size`, id: bundle.id, From 88591acc0396116b6cc1ac1cf2a3560696d0ad96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 15 Oct 2020 17:25:55 +0200 Subject: [PATCH 08/79] [DOCS] Adds initial content to Transforms readme.md (#80630) Co-authored-by: Walter Rafelsberger Co-authored-by: Pete Harverson --- docs/developer/plugin-list.asciidoc | 4 +- x-pack/plugins/transform/readme.md | 117 ++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/transform/readme.md diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b5a810852b94d..3e849ca80db72 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -499,8 +499,8 @@ routes, etc. |Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. -|{kib-repo}blob/{branch}/x-pack/plugins/transform[transform] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/transform/readme.md[transform] +|This plugin provides access to the transforms features provided by Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/translations[translations] diff --git a/x-pack/plugins/transform/readme.md b/x-pack/plugins/transform/readme.md new file mode 100644 index 0000000000000..2ee2a7b70c5f1 --- /dev/null +++ b/x-pack/plugins/transform/readme.md @@ -0,0 +1,117 @@ +# Documentation for Transforms UI developers + +This plugin provides access to the transforms features provided by Elastic. + +## Requirements + +To use the transforms feature, you must have at least a Basic license. For more +info, refer to +[Set up transforms](https://www.elastic.co/guide/en/elasticsearch/reference/current/transform-setup.html). + + +## Setup local environment + +### Kibana + +1. Fork and clone the [Kibana repo](https://github.com/elastic/kibana). + +1. Install `nvm`, `node`, `yarn` (for example, by using Homebrew). See + [Install dependencies](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html#_install_dependencies). + +1. Make sure that Elasticsearch is deployed and running on `localhost:9200`. + +1. Navigate to the directory of the `kibana` repository on your machine. + +1. Fetch the latest changes from the repository. + +1. Checkout the branch of the version you want to use. For example, if you want + to use a 7.9 version, run `git checkout 7.9`. (Your Elasticsearch and Kibana + instances need to be the same version.) + +1. Run `nvm use`. The response shows the Node version that the environment uses. + If you need to update your Node version, the response message contains the + command you need to run to do it. + +1. Run `yarn kbn bootstrap`. It takes all the dependencies in the code and + installs/checks them. It is recommended to use it every time when you switch + between branches. + +1. Make a copy of `kibana.yml` and save as `kibana.dev.yml`. (Git will not track + the changes in `kibana.dev.yml` but yarn will use it.) + +1. Provide the appropriate password and user name in `kibana.dev.yml`. + +1. Run `yarn start` to start Kibana. + +1. Go to http://localhost:560x/xxx (check the terminal message for the exact + path). + +For more details, refer to this [getting started](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html) page. + +### Adding sample data to Kibana + +Kibana has sample data sets that you can add to your setup so that you can test +different configurations on sample data. + +1. Click the Elastic logo in the upper left hand corner of your browser to + navigate to the Kibana home page. + +1. Click *Load a data set and a Kibana dashboard*. + +1. Pick a data set or feel free to click *Add* on all of the available sample + data sets. + +These data sets are now ready to be used for creating transforms in Kibana. + +## Running tests + +### Jest tests + +Run the test following jest tests from `kibana/x-pack`. + +New snapshots, all plugins: + +``` +node scripts/jest +``` + +Update snapshots for the transform plugin: + +``` +node scripts/jest plugins/transform -u +``` + +Update snapshots for a specific directory only: + +``` +node scripts/jest x-pack/plugins/transform/public/app/sections +``` + +Run tests with verbose output: + +``` +node scripts/jest plugins/transform --verbose +``` + +### Functional tests + +Before running the test server, make sure to quit all other instances of +Elasticsearch. + +1. From one terminal, in the x-pack directory, run: + + node scripts/functional_tests_server.js --config test/functional/config.js + + This command starts an Elasticsearch and Kibana instance that the tests will be run against. + +1. In another tab, run the following command to perform API integration tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag transform --config test/api_integration/config + + The transform API integration tests are located in `x-pack/test/api_integration/apis/transform`. + +1. In another tab, run the following command to perform UI functional tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag transform + + The transform functional tests are located in `x-pack/test/functional/apps/transform`. From 3b62b95dc6eea6f9ba200514bfcac83e4f19eed3 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Thu, 15 Oct 2020 11:28:50 -0400 Subject: [PATCH 09/79] [Security Solution][Resolver]Adjust layout and stacking for submenu/node (#80607) * [Security Solution][Resolver]Adjust layout and stacking for submenu/node Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/resolver/view/edge_line.tsx | 1 + .../resolver/view/process_event_dot.tsx | 5 ++++- .../public/resolver/view/styles.tsx | 20 +++++++++++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 777a7292e9c23..411e4b3e3a5b0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -25,6 +25,7 @@ const StyledEdgeLine = styled.div` return `${fontSize(props.magFactorX, 12, 8.5)}px`; }}; background-color: ${(props) => props.resolverEdgeColor}; + z-index: 10; `; interface StyledElapsedTime { diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index d93b46dcb0620..7968b4a3a1fd0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -62,6 +62,7 @@ const StyledDescriptionText = styled.div` text-align: left; text-transform: uppercase; width: fit-content; + z-index: 40; `; const StyledOuterGroup = styled.g` @@ -311,6 +312,7 @@ const UnstyledProcessEventDot = React.memo( outline: 'transparent', border: 'none', pointerEvents: 'none', + zIndex: 30, }} > @@ -391,6 +393,7 @@ const UnstyledProcessEventDot = React.memo( backgroundColor: colorMap.resolverBackground, alignSelf: 'flex-start', padding: 0, + zIndex: 40, }} > Date: Thu, 15 Oct 2020 19:07:25 +0300 Subject: [PATCH 10/79] [Security Solution][Case] Fix Jira's parent issue placeholder (#80619) --- .../public/cases/components/settings/jira/translations.ts | 4 ++-- .../components/builtin_action_types/jira/translations.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts b/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts index 54c46f064aa75..05a05ac6dd586 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts @@ -36,14 +36,14 @@ export const GET_ISSUE_API_ERROR = (id: string) => export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 2517552304d8d..019133b03d55f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -151,14 +151,14 @@ export const GET_ISSUE_API_ERROR = (id: string) => export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); From 62c93aa009f467af6ae22a78f745ea4b27391afc Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 15 Oct 2020 12:16:51 -0400 Subject: [PATCH 11/79] Fix sorting of alerts (#80546) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/alerts/fetch_status.test.ts | 73 +++++++++++++++---- .../server/lib/alerts/fetch_status.ts | 10 ++- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index fdd7253550624..824eeab7245b4 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -7,12 +7,16 @@ import { fetchStatus } from './fetch_status'; import { AlertUiState, AlertState } from '../../alerts/types'; import { AlertSeverity } from '../../../common/enums'; -import { ALERT_CPU_USAGE, ALERT_CLUSTER_HEALTH } from '../../../common/constants'; +import { + ALERT_CPU_USAGE, + ALERT_CLUSTER_HEALTH, + ALERT_DISK_USAGE, + ALERT_MISSING_MONITORING_DATA, +} from '../../../common/constants'; describe('fetchStatus', () => { const alertType = ALERT_CPU_USAGE; const alertTypes = [alertType]; - const log = { warn: jest.fn() }; const start = 0; const end = 0; const id = 1; @@ -53,6 +57,7 @@ describe('fetchStatus', () => { afterEach(() => { (alertsClient.find as jest.Mock).mockClear(); (alertsClient.getAlertState as jest.Mock).mockClear(); + alertStates.length = 0; }); it('should fetch from the alerts client', async () => { @@ -62,8 +67,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status).toEqual({ monitoring_alert_cpu_usage: { @@ -98,8 +102,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); @@ -126,8 +129,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, customStart, - customEnd, - log as any + customEnd ); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); @@ -141,8 +143,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` @@ -160,8 +161,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status[alertType].states.length).toEqual(0); }); @@ -178,8 +178,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status).toEqual({}); }); @@ -197,9 +196,51 @@ describe('fetchStatus', () => { [ALERT_CLUSTER_HEALTH], defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); + + it('should sort the alerts', async () => { + const customAlertsClient = { + find: jest.fn(() => ({ + total: 1, + data: [ + { + id, + }, + ], + })), + getAlertState: jest.fn(() => ({ + alertInstances: { + abc: { + state: { + alertStates: [ + { + cluster: defaultClusterState, + ui: { + ...defaultUiState, + isFiring: true, + }, + }, + ], + }, + }, + }, + })), + }; + const status = await fetchStatus( + customAlertsClient as any, + licenseService as any, + [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], + defaultClusterState.clusterUuid, + start, + end + ); + expect(Object.keys(status)).toEqual([ + ALERT_CPU_USAGE, + ALERT_DISK_USAGE, + ALERT_MISSING_MONITORING_DATA, + ]); + }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 49e688fafbee5..ed49f42e4908c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -18,8 +18,9 @@ export async function fetchStatus( clusterUuid: string, start: number, end: number, - filters: CommonAlertFilter[] + filters: CommonAlertFilter[] = [] ): Promise<{ [type: string]: CommonAlertStatus }> { + const types: Array<{ type: string; result: CommonAlertStatus }> = []; const byType: { [type: string]: CommonAlertStatus } = {}; await Promise.all( (alertTypes || ALERTS).map(async (type) => { @@ -39,7 +40,7 @@ export async function fetchStatus( alert: serialized, }; - byType[type] = result; + types.push({ type, result }); const id = alert.getId(); if (!id) { @@ -75,5 +76,10 @@ export async function fetchStatus( }) ); + types.sort((a, b) => (a.type === b.type ? 0 : a.type.length > b.type.length ? 1 : -1)); + for (const { type, result } of types) { + byType[type] = result; + } + return byType; } From e1456372dae7f3d2bbdfc379309d43e892a7468d Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Thu, 15 Oct 2020 17:30:09 +0100 Subject: [PATCH 12/79] [SecuritySolution] Replace act with waitFor (#80648) * replace act with waitFor * update unit test * update unit test * update unit test --- .../embeddables/embedded_map.test.tsx | 73 +++++++++---------- .../export_timeline/export_timeline.test.tsx | 31 ++++---- 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index 219409b10be6c..f927173b144d6 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -7,7 +7,7 @@ import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; import * as redux from 'react-redux'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; @@ -107,20 +107,19 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); + const wrapper: ReactWrapper = mount( + + + + ); - wrapper!.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); }); test('renders IndexPatternsMissingPrompt', async () => { @@ -132,20 +131,18 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); - - wrapper!.update(); + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); }); test('renders Loader', async () => { @@ -154,19 +151,17 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(null); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); - - wrapper!.update(); + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx index d0cfbaccde7dd..31051a51a58d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx @@ -12,7 +12,7 @@ import { mockSelectedTimeline } from './mocks'; import * as i18n from '../translations'; import { ReactWrapper, mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; jest.mock('../translations', () => { @@ -102,15 +102,14 @@ describe('TimelineDownloader', () => { ...defaultTestProps, }; - await act(() => { - wrapper = mount(); - }); - - wrapper.update(); + wrapper = mount(); + await waitFor(() => { + wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES - ); + expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES + ); + }); }); test('With correct toast message on success for exported templates', async () => { @@ -119,15 +118,15 @@ describe('TimelineDownloader', () => { }; (useParams as jest.Mock).mockReturnValue({ tabName: 'template' }); - await act(() => { - wrapper = mount(); - }); + wrapper = mount(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES - ); + expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES + ); + }); }); }); }); From cd9381c1181b2f7557ef4934ecb5ede99e7b7989 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 15 Oct 2020 12:33:53 -0400 Subject: [PATCH 13/79] [Security Solution][Resolver] Data stream fields being populated (#80216) * Data stream fields being populated * Adding some comments * Switching data stream options to specific functions * Removing unneeded import * Refactoring based on Brent's feedback --- .../common/endpoint/generate_data.test.ts | 51 ++++- .../common/endpoint/generate_data.ts | 190 ++++++++++++++---- .../common/endpoint/index_data.ts | 83 +++++--- .../common/endpoint/types/index.ts | 16 ++ .../pages/endpoint_hosts/view/index.test.tsx | 8 +- .../endpoint/resolver_generator_script.ts | 4 +- .../routes/resolver/utils/pagination.test.ts | 2 +- .../apis/package.ts | 27 ++- .../apis/resolver/children.ts | 38 +++- .../apis/resolver/entity_id.ts | 24 ++- 10 files changed, 346 insertions(+), 97 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 7e3b3d125fb5d..66119e098238e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -26,6 +26,53 @@ interface Node { parent_entity_id?: string; } +describe('data generator data streams', () => { + // these tests cast the result of the generate methods so that we can specifically compare the `data_stream` fields + it('creates a generator with default data streams', () => { + const generator = new EndpointDocGenerator('seed'); + expect(generator.generateHostMetadata().data_stream).toEqual({ + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', + }); + expect(generator.generatePolicyResponse().data_stream).toEqual({ + type: 'metrics', + dataset: 'endpoint.policy', + namespace: 'default', + }); + expect(generator.generateEvent().data_stream).toEqual({ + type: 'logs', + dataset: 'endpoint.events.process', + namespace: 'default', + }); + expect(generator.generateAlert().data_stream).toEqual({ + type: 'logs', + dataset: 'endpoint.alerts', + namespace: 'default', + }); + }); + + it('creates a generator with custom data streams', () => { + const metadataDataStream = { type: 'meta', dataset: 'dataset', namespace: 'name' }; + const policyDataStream = { type: 'policy', dataset: 'fake', namespace: 'something' }; + const eventsDataStream = { type: 'events', dataset: 'events stuff', namespace: 'name' }; + const alertsDataStream = { type: 'alerts', dataset: 'alerts stuff', namespace: 'name' }; + const generator = new EndpointDocGenerator('seed'); + expect(generator.generateHostMetadata(0, metadataDataStream).data_stream).toStrictEqual( + metadataDataStream + ); + expect(generator.generatePolicyResponse({ policyDataStream }).data_stream).toStrictEqual( + policyDataStream + ); + expect(generator.generateEvent({ eventsDataStream }).data_stream).toStrictEqual( + eventsDataStream + ); + expect(generator.generateAlert({ alertsDataStream }).data_stream).toStrictEqual( + alertsDataStream + ); + }); +}); + describe('data generator', () => { let generator: EndpointDocGenerator; beforeEach(() => { @@ -69,7 +116,7 @@ describe('data generator', () => { it('creates policy response documents', () => { const timestamp = new Date().getTime(); - const hostPolicyResponse = generator.generatePolicyResponse(timestamp); + const hostPolicyResponse = generator.generatePolicyResponse({ ts: timestamp }); expect(hostPolicyResponse['@timestamp']).toEqual(timestamp); expect(hostPolicyResponse.event.created).toEqual(timestamp); expect(hostPolicyResponse.Endpoint).not.toBeNull(); @@ -80,7 +127,7 @@ describe('data generator', () => { it('creates alert event documents', () => { const timestamp = new Date().getTime(); - const alert = generator.generateAlert(timestamp); + const alert = generator.generateAlert({ ts: timestamp }); expect(alert['@timestamp']).toEqual(timestamp); expect(alert.event?.action).not.toBeNull(); expect(alert.Endpoint).not.toBeNull(); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index f0254616e6c9d..07b230ffc6cc5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -7,6 +7,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; import { AlertEvent, + DataStream, EndpointStatus, Host, HostMetadata, @@ -59,6 +60,7 @@ interface EventOptions { pid?: number; parentPid?: number; extensions?: object; + eventsDataStream?: DataStream; } const Windows: OSFields[] = [ @@ -330,6 +332,8 @@ export interface TreeOptions { percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; ancestryArraySize?: number; + eventsDataStream?: DataStream; + alertsDataStream?: DataStream; } type TreeOptionDefaults = Required; @@ -351,19 +355,51 @@ export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults percentTerminated: options?.percentTerminated ?? 100, alwaysGenMaxChildrenPerNode: options?.alwaysGenMaxChildrenPerNode ?? false, ancestryArraySize: options?.ancestryArraySize ?? ANCESTRY_LIMIT, + eventsDataStream: options?.eventsDataStream ?? eventsDefaultDataStream, + alertsDataStream: options?.alertsDataStream ?? alertsDefaultDataStream, }; } +const metadataDefaultDataStream = { + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', +}; + +const policyDefaultDataStream = { + type: 'metrics', + dataset: 'endpoint.policy', + namespace: 'default', +}; + +const eventsDefaultDataStream = { + type: 'logs', + dataset: 'endpoint.events.process', + namespace: 'default', +}; + +const alertsDefaultDataStream = { + type: 'logs', + dataset: 'endpoint.alerts', + namespace: 'default', +}; + export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; sequence: number = 0; + /** + * The EndpointDocGenerator parameters + * + * @param seed either a string to seed the random number generator or a random number generator function + */ constructor(seed: string | seedrandom.prng = Math.random().toString()) { if (typeof seed === 'string') { this.random = seedrandom(seed); } else { this.random = seed; } + this.commonInfo = this.createHostData(); } @@ -383,6 +419,21 @@ export class EndpointDocGenerator { this.commonInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES); } + /** + * Parses an index and returns the data stream fields extracted from the index. + * + * @param index the index name to parse into the data stream parts + */ + public static createDataStreamFromIndex(index: string): DataStream { + // e.g. logs-endpoint.events.network-default + const parts = index.split('-'); + return { + type: parts[0], // logs + dataset: parts[1], // endpoint.events.network + namespace: parts[2], // default + }; + } + private createHostData(): HostInfo { const hostName = this.randomHostname(); return { @@ -417,8 +468,12 @@ export class EndpointDocGenerator { /** * Creates a host metadata document * @param ts - Timestamp to put in the event + * @param metadataDataStream the values to populate the data_stream fields when generating metadata documents */ - public generateHostMetadata(ts = new Date().getTime()): HostMetadata { + public generateHostMetadata( + ts = new Date().getTime(), + metadataDataStream = metadataDefaultDataStream + ): HostMetadata { return { '@timestamp': ts, event: { @@ -432,6 +487,7 @@ export class EndpointDocGenerator { dataset: 'endpoint.metadata', }, ...this.commonInfo, + data_stream: metadataDataStream, }; } @@ -441,15 +497,24 @@ export class EndpointDocGenerator { * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists * @param ancestry - an array of ancestors for the generated alert + * @param alertsDataStream the values to populate the data_stream fields when generating alert documents */ - public generateAlert( + public generateAlert({ ts = new Date().getTime(), entityID = this.randomString(10), - parentEntityID?: string, - ancestry: string[] = [] - ): AlertEvent { + parentEntityID, + ancestry = [], + alertsDataStream = alertsDefaultDataStream, + }: { + ts?: number; + entityID?: string; + parentEntityID?: string; + ancestry?: string[]; + alertsDataStream?: DataStream; + } = {}): AlertEvent { return { ...this.commonInfo, + data_stream: alertsDataStream, '@timestamp': ts, ecs: { version: '1.4.0', @@ -598,6 +663,7 @@ export class EndpointDocGenerator { return {}; })(options.eventCategory); return { + data_stream: options?.eventsDataStream ?? eventsDefaultDataStream, '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), agent: { ...this.commonInfo.agent, type: 'endpoint' }, ecs: { @@ -813,6 +879,7 @@ export class EndpointDocGenerator { const startDate = new Date().getTime(); const root = this.generateEvent({ timestamp: startDate + 1000, + eventsDataStream: opts.eventsDataStream, }); events.push(root); let ancestor = root; @@ -824,18 +891,24 @@ export class EndpointDocGenerator { secBeforeAlert: number, eventList: Event[] ) => { - for (const relatedAlert of this.relatedAlertsGenerator(node, alertsPerNode, secBeforeAlert)) { + for (const relatedAlert of this.relatedAlertsGenerator({ + node, + relatedAlerts: alertsPerNode, + alertCreationTime: secBeforeAlert, + alertsDataStream: opts.alertsDataStream, + })) { eventList.push(relatedAlert); } }; const addRelatedEvents = (node: Event, secBeforeEvent: number, eventList: Event[]) => { - for (const relatedEvent of this.relatedEventsGenerator( + for (const relatedEvent of this.relatedEventsGenerator({ node, - opts.relatedEvents, - secBeforeEvent, - opts.relatedEventsOrdered - )) { + relatedEvents: opts.relatedEvents, + processDuration: secBeforeEvent, + ordered: opts.relatedEventsOrdered, + eventsDataStream: opts.eventsDataStream, + })) { eventList.push(relatedEvent); } }; @@ -857,6 +930,7 @@ export class EndpointDocGenerator { parentEntityID: parentEntityIDSafeVersion(root), eventCategory: ['process'], eventType: ['end'], + eventsDataStream: opts.eventsDataStream, }) ); } @@ -877,6 +951,7 @@ export class EndpointDocGenerator { ancestryArrayLimit: opts.ancestryArraySize, parentPid: firstNonNullValue(ancestor.process?.pid), pid: this.randomN(5000), + eventsDataStream: opts.eventsDataStream, }); events.push(ancestor); timestamp = timestamp + 1000; @@ -892,6 +967,7 @@ export class EndpointDocGenerator { eventType: ['end'], ancestry: ancestryArray(ancestor), ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }) ); } @@ -912,12 +988,13 @@ export class EndpointDocGenerator { timestamp = timestamp + 1000; events.push( - this.generateAlert( - timestamp, - entityIDSafeVersion(ancestor), - parentEntityIDSafeVersion(ancestor), - ancestryArray(ancestor) - ) + this.generateAlert({ + ts: timestamp, + entityID: entityIDSafeVersion(ancestor), + parentEntityID: parentEntityIDSafeVersion(ancestor), + ancestry: ancestryArray(ancestor), + alertsDataStream: opts.alertsDataStream, + }) ); return events; } @@ -973,6 +1050,7 @@ export class EndpointDocGenerator { parentEntityID: currentStateEntityID, ancestry, ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }); maxChildren = this.randomN(opts.children + 1); @@ -996,16 +1074,23 @@ export class EndpointDocGenerator { eventType: ['end'], ancestry, ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }); } if (this.randomN(100) < opts.percentWithRelated) { - yield* this.relatedEventsGenerator( - child, - opts.relatedEvents, + yield* this.relatedEventsGenerator({ + node: child, + relatedEvents: opts.relatedEvents, processDuration, - opts.relatedEventsOrdered - ); - yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); + ordered: opts.relatedEventsOrdered, + eventsDataStream: opts.eventsDataStream, + }); + yield* this.relatedAlertsGenerator({ + node: child, + relatedAlerts: opts.relatedAlerts, + alertCreationTime: processDuration, + alertsDataStream: opts.alertsDataStream, + }); } } } @@ -1019,12 +1104,19 @@ export class EndpointDocGenerator { * @param ordered - if true the events will have an increasing timestamp, otherwise their timestamp will be random but * guaranteed to be greater than or equal to the originating event */ - public *relatedEventsGenerator( - node: Event, - relatedEvents: RelatedEventInfo[] | number = 10, - processDuration: number = 6 * 3600, - ordered: boolean = false - ) { + public *relatedEventsGenerator({ + node, + relatedEvents = 10, + processDuration = 6 * 3600, + ordered = false, + eventsDataStream = eventsDefaultDataStream, + }: { + node: Event; + relatedEvents?: RelatedEventInfo[] | number; + processDuration?: number; + ordered?: boolean; + eventsDataStream?: DataStream; + }) { let relatedEventsInfo: RelatedEventInfo[]; const nodeTimestamp = timestampSafeVersion(node) ?? 0; let ts = nodeTimestamp + 1; @@ -1056,6 +1148,7 @@ export class EndpointDocGenerator { eventCategory: eventInfo.category, eventType: eventInfo.creationType, ancestry: ancestryArray(node), + eventsDataStream, }); } } @@ -1067,19 +1160,26 @@ export class EndpointDocGenerator { * @param relatedAlerts - number which defines the number of related alerts to create * @param alertCreationTime - maximum number of seconds after process event that related alert timestamp can be */ - public *relatedAlertsGenerator( - node: Event, - relatedAlerts: number = 3, - alertCreationTime: number = 6 * 3600 - ) { + public *relatedAlertsGenerator({ + node, + relatedAlerts = 3, + alertCreationTime = 6 * 3600, + alertsDataStream = alertsDefaultDataStream, + }: { + node: Event; + relatedAlerts: number; + alertCreationTime: number; + alertsDataStream: DataStream; + }) { for (let i = 0; i < relatedAlerts; i++) { const ts = (timestampSafeVersion(node) ?? 0) + this.randomN(alertCreationTime) * 1000; - yield this.generateAlert( + yield this.generateAlert({ ts, - entityIDSafeVersion(node), - parentEntityIDSafeVersion(node), - ancestryArray(node) - ); + entityID: entityIDSafeVersion(node), + parentEntityID: parentEntityIDSafeVersion(node), + ancestry: ancestryArray(node), + alertsDataStream, + }); } } @@ -1227,15 +1327,21 @@ export class EndpointDocGenerator { /** * Generates a Host Policy response message */ - public generatePolicyResponse( + public generatePolicyResponse({ ts = new Date().getTime(), - allStatus?: HostPolicyResponseActionStatus - ): HostPolicyResponse { + allStatus, + policyDataStream = policyDefaultDataStream, + }: { + ts?: number; + allStatus?: HostPolicyResponseActionStatus; + policyDataStream?: DataStream; + } = {}): HostPolicyResponse { const policyVersion = this.seededUUIDv4(); const status = () => { return allStatus || this.randomHostPolicyResponseActionStatus(); }; return { + data_stream: policyDataStream, '@timestamp': ts, agent: { id: this.commonInfo.agent.id, diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index bf3d12f231c86..c0c70f9ca11af 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -52,10 +52,9 @@ export async function indexHostsAndAlerts( const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); // Keep a map of host applied policy ids (fake) to real ingest package configs (policy record) const realPolicies: Record = {}; - for (let i = 0; i < numHosts; i++) { const generator = new EndpointDocGenerator(random); - await indexHostDocs( + await indexHostDocs({ numDocs, client, kbnClient, @@ -63,10 +62,17 @@ export async function indexHostsAndAlerts( epmEndpointPackage, metadataIndex, policyResponseIndex, - fleet, - generator - ); - await indexAlerts(client, eventIndex, alertIndex, generator, alertsPerHost, options); + enrollFleet: fleet, + generator, + }); + await indexAlerts({ + client, + eventIndex, + alertIndex, + generator, + numAlerts: alertsPerHost, + options, + }); } await client.indices.refresh({ index: eventIndex, @@ -81,17 +87,27 @@ function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function indexHostDocs( - numDocs: number, - client: Client, - kbnClient: KbnClientWithApiKeySupport, - realPolicies: Record, - epmEndpointPackage: GetPackagesResponse['response'][0], - metadataIndex: string, - policyResponseIndex: string, - enrollFleet: boolean, - generator: EndpointDocGenerator -) { +async function indexHostDocs({ + numDocs, + client, + kbnClient, + realPolicies, + epmEndpointPackage, + metadataIndex, + policyResponseIndex, + enrollFleet, + generator, +}: { + numDocs: number; + client: Client; + kbnClient: KbnClientWithApiKeySupport; + realPolicies: Record; + epmEndpointPackage: GetPackagesResponse['response'][0]; + metadataIndex: string; + policyResponseIndex: string; + enrollFleet: boolean; + generator: EndpointDocGenerator; +}) { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); let hostMetadata: HostMetadata; @@ -102,7 +118,10 @@ async function indexHostDocs( generator.updateHostData(); generator.updateHostPolicyData(); - hostMetadata = generator.generateHostMetadata(timestamp - timeBetweenDocs * (numDocs - j - 1)); + hostMetadata = generator.generateHostMetadata( + timestamp - timeBetweenDocs * (numDocs - j - 1), + EndpointDocGenerator.createDataStreamFromIndex(metadataIndex) + ); if (enrollFleet) { const { id: appliedPolicyId, name: appliedPolicyName } = hostMetadata.Endpoint.policy.applied; @@ -156,20 +175,30 @@ async function indexHostDocs( }); await client.index({ index: policyResponseIndex, - body: generator.generatePolicyResponse(timestamp - timeBetweenDocs * (numDocs - j - 1)), + body: generator.generatePolicyResponse({ + ts: timestamp - timeBetweenDocs * (numDocs - j - 1), + policyDataStream: EndpointDocGenerator.createDataStreamFromIndex(policyResponseIndex), + }), op_type: 'create', }); } } -async function indexAlerts( - client: Client, - eventIndex: string, - alertIndex: string, - generator: EndpointDocGenerator, - numAlerts: number, - options: TreeOptions = {} -) { +async function indexAlerts({ + client, + eventIndex, + alertIndex, + generator, + numAlerts, + options = {}, +}: { + client: Client; + eventIndex: string; + alertIndex: string; + generator: EndpointDocGenerator; + numAlerts: number; + options: TreeOptions; +}) { const alertGenerator = generator.alertsGenerator(numAlerts, options); let result = alertGenerator.next(); while (!result.done) { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 510f1833b793b..f2033e064ef72 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -300,6 +300,15 @@ export interface HostResultList { query_strategy_version: MetadataQueryStrategyVersions; } +/** + * The data_stream fields in an elasticsearch document. + */ +export interface DataStream { + dataset: string; + namespace: string; + type: string; +} + /** * Operating System metadata. */ @@ -556,6 +565,7 @@ export type HostMetadata = Immutable<{ version: string; }; host: Host; + data_stream: DataStream; }>; export interface LegacyEndpointEvent { @@ -675,6 +685,11 @@ export type SafeEndpointEvent = Partial<{ version: ECSField; type: ECSField; }>; + data_stream: Partial<{ + type: ECSField; + dataset: ECSField; + namespace: ECSField; + }>; ecs: Partial<{ version: ECSField; }>; @@ -1002,6 +1017,7 @@ interface HostPolicyResponseAppliedArtifact { */ export interface HostPolicyResponse { '@timestamp': number; + data_stream: DataStream; elastic: { agent: { id: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 12a76ae0772a3..d785e3b3a131a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -702,10 +702,10 @@ describe('when on the list page', () => { }); it('should not show any numbered badges if all actions are successful', () => { - const policyResponse = docGenerator.generatePolicyResponse( - new Date().getTime(), - HostPolicyResponseActionStatus.success - ); + const policyResponse = docGenerator.generatePolicyResponse({ + ts: new Date().getTime(), + allStatus: HostPolicyResponseActionStatus.success, + }); reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedEndpointPolicyResponse', diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 9fa2257afd411..c513c4576d890 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -10,7 +10,7 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { KbnClient, ToolingLog } from '@kbn/dev-utils'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; -import { ANCESTRY_LIMIT } from '../../common/endpoint/generate_data'; +import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../ingest_manager/common/constants'; import { CreateFleetSetupResponse, @@ -250,6 +250,8 @@ async function main() { percentTerminated: argv.percentTerminated, alwaysGenMaxChildrenPerNode: argv.maxChildrenPerNode, ancestryArraySize: argv.ancestryArraySize, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.eventIndex), + alertsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.alertIndex), } ); console.log(`Creating and indexing documents took: ${new Date().getTime() - startTime}ms`); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index 55965e5a9c70e..d5297072388e7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -20,7 +20,7 @@ describe('Pagination', () => { }; describe('cursor', () => { const root = generator.generateEvent(); - const events = Array.from(generator.relatedEventsGenerator(root, 5)); + const events = Array.from(generator.relatedEventsGenerator({ node: root, relatedEvents: 5 })); it('does build a cursor when received the same number of events as was requested', () => { expect(PaginationBuilder.buildCursorRequestLimit(4, events)).not.toBeNull(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts index afbf0dcd7bd13..8dc78ed71d0b6 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -74,7 +74,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('handles events without the `network.protocol` field being defined', async () => { - const eventWithoutNetworkObject = generator.generateEvent(); + const eventWithoutNetworkObject = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), + }); // ensure that `network.protocol` does not exist in the event to test that the pipeline handles those type of events delete eventWithoutNetworkObject.network; @@ -137,8 +139,10 @@ export default function ({ getService }: FtrProviderContext) { let genData: InsertedEvents; before(async () => { - event = generator.generateEvent(); - genData = await resolver.insertEvents([event]); + event = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); + genData = await resolver.insertEvents([event], processEventsIndex); }); after(async () => { @@ -158,20 +162,29 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // 46.239.193.5 should be in Iceland // 8.8.8.8 should be in the US - const eventWithBothIPs = generator.generateEvent({ + const eventWithBothIPsNetwork = generator.generateEvent({ extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), }); - const eventWithSourceOnly = generator.generateEvent({ + const eventWithSourceOnlyNetwork = generator.generateEvent({ extensions: { source: { ip: '8.8.8.8' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), }); networkIndexData = await resolver.insertEvents( - [eventWithBothIPs, eventWithSourceOnly], + [eventWithBothIPsNetwork, eventWithSourceOnlyNetwork], networkIndex ); - processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + const eventWithBothIPsProcess = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); + processIndexData = await resolver.insertEvents( + [eventWithBothIPsProcess], + processEventsIndex + ); }); after(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts index 49e24ff67fa77..b56dea94ab569 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -22,7 +22,7 @@ import { Event, EndpointDocGenerator, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { InsertedEvents } from '../../services/resolver'; +import { InsertedEvents, processEventsIndex } from '../../services/resolver'; import { createAncestryArray } from './common'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { @@ -42,25 +42,33 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> infoEvent -> startEvent -> execEvent - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); infoEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(infoEvent), ancestry: createAncestryArray([infoEvent, origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(startEvent), ancestry: createAncestryArray([startEvent, infoEvent]), eventType: ['change'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { @@ -88,11 +96,14 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> (infoEvent, startEvent, execEvent are all for the same node) - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); infoEvent = generator.generateEvent({ @@ -100,6 +111,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), entityID: entityIDSafeVersion(startEvent), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ @@ -107,8 +119,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), eventType: ['change'], entityID: entityIDSafeVersion(startEvent), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { @@ -141,11 +157,14 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> (infoEvent, startEvent, execEvent are all for the same node) - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); infoEvent = generator.generateEvent({ @@ -154,6 +173,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), entityID: entityIDSafeVersion(startEvent), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ @@ -162,8 +182,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), eventType: ['change'], entityID: entityIDSafeVersion(startEvent), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index e6d5e8fccd00d..f9492e6291684 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -15,7 +15,7 @@ import { EndpointDocGenerator, Event, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { InsertedEvents } from '../../services/resolver'; +import { InsertedEvents, processEventsIndex } from '../../services/resolver'; import { createAncestryArray } from './common'; export default function ({ getService }: FtrProviderContext) { @@ -34,9 +34,12 @@ export default function ({ getService }: FtrProviderContext) { let origin: Event; let genData: InsertedEvents; before(async () => { - origin = generator.generateEvent({ parentEntityID: 'a' }); + origin = generator.generateEvent({ + parentEntityID: 'a', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); setEntityIDEmptyString(origin); - genData = await resolver.insertEvents([origin]); + genData = await resolver.insertEvents([origin], processEventsIndex); }); after(async () => { @@ -63,10 +66,14 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // construct a tree with an origin and two direct children. One child will not have an entity_id. That child // should not be returned by the backend. - origin = generator.generateEvent({ entityID: 'a' }); + origin = generator.generateEvent({ + entityID: 'a', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); childNoEntityID = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); // force it to be empty setEntityIDEmptyString(childNoEntityID); @@ -75,9 +82,10 @@ export default function ({ getService }: FtrProviderContext) { entityID: 'b', parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); events = [origin, childNoEntityID, childWithEntityID]; - genData = await resolver.insertEvents(events); + genData = await resolver.insertEvents(events, processEventsIndex); }); after(async () => { @@ -106,17 +114,20 @@ export default function ({ getService }: FtrProviderContext) { // entity_ids in the ancestry array. This is to make sure that the backend will not query for that event. ancestor2 = generator.generateEvent({ entityID: '2', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); ancestor1 = generator.generateEvent({ entityID: '1', parentEntityID: entityIDSafeVersion(ancestor2), ancestry: createAncestryArray([ancestor2]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); // we'll insert an event that doesn't have an entity id so if the backend does search for it, it should be // returned and our test should fail ancestorNoEntityID = generator.generateEvent({ ancestry: createAncestryArray([ancestor2]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); setEntityIDEmptyString(ancestorNoEntityID); @@ -124,10 +135,11 @@ export default function ({ getService }: FtrProviderContext) { entityID: 'a', parentEntityID: entityIDSafeVersion(ancestor1), ancestry: ['', ...createAncestryArray([ancestor2])], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); events = [origin, ancestor1, ancestor2, ancestorNoEntityID]; - genData = await resolver.insertEvents(events); + genData = await resolver.insertEvents(events, processEventsIndex); }); after(async () => { From 07c1284e9db2a180fc660302141455e3c33dae6d Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 15 Oct 2020 09:56:10 -0700 Subject: [PATCH 14/79] [kbn/bootstrap] validate that certain deps don't ship in production (#80549) Co-authored-by: spalger --- package.json | 6 +- .../elastic-eslint-config-kibana/package.json | 3 + packages/kbn-babel-preset/package.json | 3 + packages/kbn-config/package.json | 4 +- packages/kbn-dev-utils/package.json | 3 + packages/kbn-es-archiver/package.json | 3 + packages/kbn-es/package.json | 3 + .../package.json | 3 + .../kbn-eslint-plugin-eslint/package.json | 3 + packages/kbn-expect/package.json | 5 +- packages/kbn-pm/dist/index.js | 1414 +++++++++-------- packages/kbn-pm/package.json | 3 + packages/kbn-pm/src/commands/bootstrap.ts | 4 +- packages/kbn-pm/src/utils/project.ts | 4 + packages/kbn-pm/src/utils/projects_tree.ts | 4 +- ..._yarn_lock.ts => validate_dependencies.ts} | 44 +- packages/kbn-release-notes/package.json | 3 + packages/kbn-std/package.json | 8 +- packages/kbn-storybook/package.json | 3 + packages/kbn-telemetry-tools/package.json | 3 + packages/kbn-test-subj-selector/package.json | 5 +- packages/kbn-test/package.json | 3 + packages/kbn-utility-types/package.json | 3 + 23 files changed, 835 insertions(+), 702 deletions(-) rename packages/kbn-pm/src/utils/{validate_yarn_lock.ts => validate_dependencies.ts} (77%) diff --git a/package.json b/package.json index 732ee1fd3038b..951c73dc94021 100644 --- a/package.json +++ b/package.json @@ -131,10 +131,7 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/logging": "1.0.0", - "@kbn/pm": "1.0.0", "@kbn/std": "1.0.0", - "@kbn/telemetry-tools": "1.0.0", - "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ace": "1.0.0", "@kbn/monaco": "1.0.0", @@ -247,8 +244,11 @@ "@kbn/expect": "1.0.0", "@kbn/optimizer": "1.0.0", "@kbn/plugin-generator": "1.0.0", + "@kbn/pm": "1.0.0", "@kbn/release-notes": "1.0.0", + "@kbn/telemetry-tools": "1.0.0", "@kbn/test": "1.0.0", + "@kbn/test-subj-selector": "0.2.1", "@kbn/utility-types": "1.0.0", "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", diff --git a/packages/elastic-eslint-config-kibana/package.json b/packages/elastic-eslint-config-kibana/package.json index 3f2c6e9edb261..9d0d579086543 100644 --- a/packages/elastic-eslint-config-kibana/package.json +++ b/packages/elastic-eslint-config-kibana/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/elastic/kibana.git" }, + "kibana": { + "devOnly": true + }, "keywords": [], "author": "Spencer Alger ", "license": "Apache-2.0", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index 79d2fd8687dae..2fab970c5c71f 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "dependencies": { "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-export-namespace-from": "^7.10.4", diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index 6d2d56b929ead..f994836af8847 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -12,10 +12,8 @@ "dependencies": { "@elastic/safer-lodash-set": "0.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/dev-utils": "1.0.0", "@kbn/logging": "1.0.0", "@kbn/std": "1.0.0", - "@kbn/utility-types": "1.0.0", "js-yaml": "^3.14.0", "load-json-file": "^6.2.0", "lodash": "^4.17.20", @@ -24,6 +22,8 @@ "type-detect": "^4.0.8" }, "devDependencies": { + "@kbn/dev-utils": "1.0.0", + "@kbn/utility-types": "1.0.0", "typescript": "4.0.2", "tsd": "^0.13.1" } diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index a51734168cf76..7fd9a9e7d67e1 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -9,6 +9,9 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, + "kibana": { + "devOnly": true + }, "dependencies": { "@babel/core": "^7.11.6", "@kbn/utils": "1.0.0", diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 81c1747bb2727..645abd6195909 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "Apache-2.0", "main": "target/index.js", + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "rm -rf target && tsc", "kbn:watch": "rm -rf target && tsc --watch" diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index c3733094350be..6ed3ae2eb2fb7 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -4,6 +4,9 @@ "version": "1.0.0", "license": "Apache-2.0", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "node scripts/build", "kbn:watch": "node scripts/build --watch" diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 223c73e97908e..ffbd94810a405 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -5,6 +5,9 @@ "version": "2.0.0", "main": "import_resolver_kibana.js", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "repository": { "type": "git", "url": "https://github.com/elastic/kibana/tree/master/packages/kbn-eslint-import-resolver-kibana" diff --git a/packages/kbn-eslint-plugin-eslint/package.json b/packages/kbn-eslint-plugin-eslint/package.json index 026938213ac83..72b8577cb0945 100644 --- a/packages/kbn-eslint-plugin-eslint/package.json +++ b/packages/kbn-eslint-plugin-eslint/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "peerDependencies": { "eslint": "6.8.0", "babel-eslint": "^10.0.3" diff --git a/packages/kbn-expect/package.json b/packages/kbn-expect/package.json index 0975f5762fa1c..8ca37c7c88673 100644 --- a/packages/kbn-expect/package.json +++ b/packages/kbn-expect/package.json @@ -3,5 +3,8 @@ "main": "./expect.js", "version": "1.0.0", "license": "MIT", - "private": true + "private": true, + "kibana": { + "devOnly": true + } } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 2e50f4214beb4..c053445285c03 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -150,7 +150,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(128); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(501); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(144); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -8897,9 +8897,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(129); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(280); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(399); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(400); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(400); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(401); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8943,7 +8943,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(273); /* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(278); /* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(275); -/* harmony import */ var _utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(279); +/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(279); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -9002,7 +9002,7 @@ const BootstrapCommand = { const yarnLock = await Object(_utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__["readYarnLock"])(kbn); if (options.validate) { - await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + await Object(_utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_7__["validateDependencies"])(kbn, yarnLock); } await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_0__["linkProjectExecutables"])(projects, projectGraph); @@ -14713,6 +14713,10 @@ class Project { return this.json.kibana && this.json.kibana.clean || {}; } + isFlaggedAsDevOnly() { + return !!(this.json.kibana && this.json.kibana.devOnly); + } + hasScript(name) { return name in this.scripts; } @@ -37954,13 +37958,16 @@ class BootstrapCacheFile { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateYarnLock", function() { return validateYarnLock; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateDependencies", function() { return validateDependencies; }); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(276); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); -/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(113); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(131); +/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(144); +/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(280); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37984,7 +37991,9 @@ __webpack_require__.r(__webpack_exports__); -async function validateYarnLock(kbn, yarnLock) { + + +async function validateDependencies(kbn, yarnLock) { // look through all of the packages in the yarn.lock file to see if // we have accidentally installed multiple lodash v4 versions const lodash4Versions = new Set(); @@ -38005,8 +38014,8 @@ async function validateYarnLock(kbn, yarnLock) { delete yarnLock[req]; } - await Object(_fs__WEBPACK_IMPORTED_MODULE_2__["writeFile"])(kbn.getAbsolute('yarn.lock'), Object(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__["stringify"])(yarnLock), 'utf8'); - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + await Object(_fs__WEBPACK_IMPORTED_MODULE_3__["writeFile"])(kbn.getAbsolute('yarn.lock'), Object(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__["stringify"])(yarnLock), 'utf8'); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` Multiple version of lodash v4 were detected, so they have been removed from the yarn.lock file. Please rerun yarn kbn bootstrap to coalese the @@ -38025,7 +38034,7 @@ async function validateYarnLock(kbn, yarnLock) { // of lodash v3 in the distributable - const prodDependencies = kbn.resolveAllProductionDependencies(yarnLock, _log__WEBPACK_IMPORTED_MODULE_3__["log"]); + const prodDependencies = kbn.resolveAllProductionDependencies(yarnLock, _log__WEBPACK_IMPORTED_MODULE_4__["log"]); const lodash3Versions = new Set(); for (const dep of prodDependencies.values()) { @@ -38036,7 +38045,7 @@ async function validateYarnLock(kbn, yarnLock) { if (lodash3Versions.size) { - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` Due to changes in the yarn.lock file and/or package.json files a version of lodash 3 is now included in the production dependencies. To reduce the size of @@ -38088,7 +38097,7 @@ async function validateYarnLock(kbn, yarnLock) { }) => ` ${range} => ${projects.map(p => p.name).join(', ')}`)], []).join('\n '); if (duplicateRanges) { - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` [single_version_dependencies] Multiple version ranges for the same dependency were found declared across different package.json files. Please consolidate @@ -38102,21 +38111,207 @@ async function validateYarnLock(kbn, yarnLock) { ${duplicateRanges} `); process.exit(1); + } // look for packages that have the the `kibana.devOnly` flag in their package.json + // and make sure they aren't included in the production dependencies of Kibana + + + const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(kbn, 'kibana'); + + if (devOnlyProjectsInProduction) { + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + Some of the packages in the production dependency chain for Kibana and X-Pack are + flagged with "kibana.devOnly" in their package.json. Please check changes made to + packages and their dependencies to ensure they don't end up in production. + + The devOnly dependencies that are being dependend on in production are: + + ${Object(_projects_tree__WEBPACK_IMPORTED_MODULE_5__["treeToString"])(devOnlyProjectsInProduction).split('\n').join('\n ')} + `); + process.exit(1); } - _log__WEBPACK_IMPORTED_MODULE_3__["log"].success('yarn.lock analysis completed without any issues'); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].success('yarn.lock analysis completed without any issues'); +} + +function getDevOnlyProductionDepsTree(kbn, projectName) { + const project = kbn.getProject(projectName); + const childProjectNames = [...Object.keys(project.productionDependencies).filter(name => kbn.hasProject(name)), ...(projectName === 'kibana' ? ['x-pack'] : [])]; + const children = childProjectNames.map(n => getDevOnlyProductionDepsTree(kbn, n)).filter(t => !!t); + + if (!children.length && !project.isFlaggedAsDevOnly()) { + return; + } + + const tree = { + name: project.isFlaggedAsDevOnly() ? chalk__WEBPACK_IMPORTED_MODULE_2___default.a.red.bold(projectName) : projectName, + children + }; + return tree; } /***/ }), /* 280 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "renderProjectsTree", function() { return renderProjectsTree; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "treeToString", function() { return treeToString; }); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(113); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +const projectKey = Symbol('__project'); +function renderProjectsTree(rootPath, projects) { + const projectsTree = buildProjectsTree(rootPath, projects); + return treeToString(createTreeStructure(projectsTree)); +} +function treeToString(tree) { + return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); +} + +function childrenToStrings(tree, treePrefix) { + if (tree === undefined) { + return []; + } + + let strings = []; + tree.forEach((node, index) => { + const isLastNode = tree.length - 1 === index; + const nodePrefix = isLastNode ? '└── ' : '├── '; + const childPrefix = isLastNode ? ' ' : '│ '; + const childrenPrefix = treePrefix + childPrefix; + strings.push(`${treePrefix}${nodePrefix}${node.name}`); + strings = strings.concat(childrenToStrings(node.children, childrenPrefix)); + }); + return strings; +} + +function createTreeStructure(tree) { + let name; + const children = []; + + for (const [dir, project] of tree.entries()) { + // This is a leaf node (aka a project) + if (typeof project === 'string') { + name = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(project); + continue; + } // If there's only one project and the key indicates it's a leaf node, we + // know that we're at a package folder that contains a package.json, so we + // "inline it" so we don't get unnecessary levels, i.e. we'll just see + // `foo` instead of `foo -> foo`. + + + if (project.size === 1 && project.has(projectKey)) { + const projectName = project.get(projectKey); + children.push({ + children: [], + name: dirOrProjectName(dir, projectName) + }); + continue; + } + + const subtree = createTreeStructure(project); // If the name is specified, we know there's a package at the "root" of the + // subtree itself. + + if (subtree.name !== undefined) { + const projectName = subtree.name; + children.push({ + children: subtree.children, + name: dirOrProjectName(dir, projectName) + }); + continue; + } // Special-case whenever we have one child, so we don't get unnecessary + // folders in the output. E.g. instead of `foo -> bar -> baz` we get + // `foo/bar/baz` instead. + + + if (subtree.children && subtree.children.length === 1) { + const child = subtree.children[0]; + const newName = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(path__WEBPACK_IMPORTED_MODULE_1___default.a.join(dir.toString(), child.name)); + children.push({ + children: child.children, + name: newName + }); + continue; + } + + children.push({ + children: subtree.children, + name: chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(dir.toString()) + }); + } + + return { + name, + children + }; +} + +function dirOrProjectName(dir, projectName) { + return dir === projectName ? chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(dir) : chalk__WEBPACK_IMPORTED_MODULE_0___default.a`{dim ${dir.toString()} ({reset.green ${projectName}})}`; +} + +function buildProjectsTree(rootPath, projects) { + const tree = new Map(); + + for (const project of projects.values()) { + if (rootPath === project.path) { + tree.set(projectKey, project.name); + } else { + const relativeProjectPath = path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(rootPath, project.path); + addProjectToTree(tree, relativeProjectPath.split(path__WEBPACK_IMPORTED_MODULE_1___default.a.sep), project); + } + } + + return tree; +} + +function addProjectToTree(tree, pathParts, project) { + if (pathParts.length === 0) { + tree.set(projectKey, project.name); + } else { + const [currentDir, ...rest] = pathParts; + + if (!tree.has(currentDir)) { + tree.set(currentDir, new Map()); + } + + const subtree = tree.get(currentDir); + addProjectToTree(subtree, rest, project); + } +} + +/***/ }), +/* 281 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(281); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(282); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(367); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(368); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -38216,21 +38411,21 @@ const CleanCommand = { }; /***/ }), -/* 281 */ +/* 282 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const path = __webpack_require__(4); -const globby = __webpack_require__(282); -const isGlob = __webpack_require__(294); -const slash = __webpack_require__(358); +const globby = __webpack_require__(283); +const isGlob = __webpack_require__(295); +const slash = __webpack_require__(359); const gracefulFs = __webpack_require__(133); -const isPathCwd = __webpack_require__(360); -const isPathInside = __webpack_require__(361); -const rimraf = __webpack_require__(362); -const pMap = __webpack_require__(363); +const isPathCwd = __webpack_require__(361); +const isPathInside = __webpack_require__(362); +const rimraf = __webpack_require__(363); +const pMap = __webpack_require__(364); const rimrafP = promisify(rimraf); @@ -38344,19 +38539,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 282 */ +/* 283 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(283); -const merge2 = __webpack_require__(284); +const arrayUnion = __webpack_require__(284); +const merge2 = __webpack_require__(285); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(285); -const dirGlob = __webpack_require__(354); -const gitignore = __webpack_require__(356); -const {FilterStream, UniqueStream} = __webpack_require__(359); +const fastGlob = __webpack_require__(286); +const dirGlob = __webpack_require__(355); +const gitignore = __webpack_require__(357); +const {FilterStream, UniqueStream} = __webpack_require__(360); const DEFAULT_FILTER = () => false; @@ -38529,7 +38724,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 283 */ +/* 284 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38541,7 +38736,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 284 */ +/* 285 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38692,17 +38887,17 @@ function pauseStreams (streams, options) { /***/ }), -/* 285 */ +/* 286 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(286); -const async_1 = __webpack_require__(315); -const stream_1 = __webpack_require__(350); -const sync_1 = __webpack_require__(351); -const settings_1 = __webpack_require__(353); -const utils = __webpack_require__(287); +const taskManager = __webpack_require__(287); +const async_1 = __webpack_require__(316); +const stream_1 = __webpack_require__(351); +const sync_1 = __webpack_require__(352); +const settings_1 = __webpack_require__(354); +const utils = __webpack_require__(288); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -38766,13 +38961,13 @@ module.exports = FastGlob; /***/ }), -/* 286 */ +/* 287 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -38837,30 +39032,30 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 287 */ +/* 288 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(288); +const array = __webpack_require__(289); exports.array = array; -const errno = __webpack_require__(289); +const errno = __webpack_require__(290); exports.errno = errno; -const fs = __webpack_require__(290); +const fs = __webpack_require__(291); exports.fs = fs; -const path = __webpack_require__(291); +const path = __webpack_require__(292); exports.path = path; -const pattern = __webpack_require__(292); +const pattern = __webpack_require__(293); exports.pattern = pattern; -const stream = __webpack_require__(313); +const stream = __webpack_require__(314); exports.stream = stream; -const string = __webpack_require__(314); +const string = __webpack_require__(315); exports.string = string; /***/ }), -/* 288 */ +/* 289 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38888,7 +39083,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 289 */ +/* 290 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38901,7 +39096,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 290 */ +/* 291 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38926,7 +39121,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 291 */ +/* 292 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38965,16 +39160,16 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 292 */ +/* 293 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const globParent = __webpack_require__(293); -const micromatch = __webpack_require__(296); -const picomatch = __webpack_require__(307); +const globParent = __webpack_require__(294); +const micromatch = __webpack_require__(297); +const picomatch = __webpack_require__(308); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; @@ -39084,13 +39279,13 @@ exports.matchAny = matchAny; /***/ }), -/* 293 */ +/* 294 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(294); +var isGlob = __webpack_require__(295); var pathPosixDirname = __webpack_require__(4).posix.dirname; var isWin32 = __webpack_require__(121).platform() === 'win32'; @@ -39132,7 +39327,7 @@ module.exports = function globParent(str, opts) { /***/ }), -/* 294 */ +/* 295 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -39142,7 +39337,7 @@ module.exports = function globParent(str, opts) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(295); +var isExtglob = __webpack_require__(296); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -39186,7 +39381,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 295 */ +/* 296 */ /***/ (function(module, exports) { /*! @@ -39212,16 +39407,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 296 */ +/* 297 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(112); -const braces = __webpack_require__(297); -const picomatch = __webpack_require__(307); -const utils = __webpack_require__(310); +const braces = __webpack_require__(298); +const picomatch = __webpack_require__(308); +const utils = __webpack_require__(311); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -39686,16 +39881,16 @@ module.exports = micromatch; /***/ }), -/* 297 */ +/* 298 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(298); -const compile = __webpack_require__(300); -const expand = __webpack_require__(304); -const parse = __webpack_require__(305); +const stringify = __webpack_require__(299); +const compile = __webpack_require__(301); +const expand = __webpack_require__(305); +const parse = __webpack_require__(306); /** * Expand the given pattern or create a regex-compatible string. @@ -39863,13 +40058,13 @@ module.exports = braces; /***/ }), -/* 298 */ +/* 299 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(299); +const utils = __webpack_require__(300); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -39902,7 +40097,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 299 */ +/* 300 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40021,14 +40216,14 @@ exports.flatten = (...args) => { /***/ }), -/* 300 */ +/* 301 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(301); -const utils = __webpack_require__(299); +const fill = __webpack_require__(302); +const utils = __webpack_require__(300); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -40085,7 +40280,7 @@ module.exports = compile; /***/ }), -/* 301 */ +/* 302 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40099,7 +40294,7 @@ module.exports = compile; const util = __webpack_require__(112); -const toRegexRange = __webpack_require__(302); +const toRegexRange = __webpack_require__(303); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -40341,7 +40536,7 @@ module.exports = fill; /***/ }), -/* 302 */ +/* 303 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40354,7 +40549,7 @@ module.exports = fill; -const isNumber = __webpack_require__(303); +const isNumber = __webpack_require__(304); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -40636,7 +40831,7 @@ module.exports = toRegexRange; /***/ }), -/* 303 */ +/* 304 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40661,15 +40856,15 @@ module.exports = function(num) { /***/ }), -/* 304 */ +/* 305 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(301); -const stringify = __webpack_require__(298); -const utils = __webpack_require__(299); +const fill = __webpack_require__(302); +const stringify = __webpack_require__(299); +const utils = __webpack_require__(300); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -40781,13 +40976,13 @@ module.exports = expand; /***/ }), -/* 305 */ +/* 306 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(298); +const stringify = __webpack_require__(299); /** * Constants @@ -40809,7 +41004,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(306); +} = __webpack_require__(307); /** * parse @@ -41121,7 +41316,7 @@ module.exports = parse; /***/ }), -/* 306 */ +/* 307 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41185,27 +41380,27 @@ module.exports = { /***/ }), -/* 307 */ +/* 308 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(308); +module.exports = __webpack_require__(309); /***/ }), -/* 308 */ +/* 309 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(309); -const parse = __webpack_require__(312); -const utils = __webpack_require__(310); -const constants = __webpack_require__(311); +const scan = __webpack_require__(310); +const parse = __webpack_require__(313); +const utils = __webpack_require__(311); +const constants = __webpack_require__(312); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -41541,13 +41736,13 @@ module.exports = picomatch; /***/ }), -/* 309 */ +/* 310 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(310); +const utils = __webpack_require__(311); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -41564,7 +41759,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(311); +} = __webpack_require__(312); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -41931,7 +42126,7 @@ module.exports = scan; /***/ }), -/* 310 */ +/* 311 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41944,7 +42139,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(311); +} = __webpack_require__(312); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -42002,7 +42197,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 311 */ +/* 312 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42188,14 +42383,14 @@ module.exports = { /***/ }), -/* 312 */ +/* 313 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(311); -const utils = __webpack_require__(310); +const constants = __webpack_require__(312); +const utils = __webpack_require__(311); /** * Constants @@ -43273,13 +43468,13 @@ module.exports = parse; /***/ }), -/* 313 */ +/* 314 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(284); +const merge2 = __webpack_require__(285); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -43296,7 +43491,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 314 */ +/* 315 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43313,14 +43508,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 315 */ +/* 316 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(316); -const provider_1 = __webpack_require__(343); +const stream_1 = __webpack_require__(317); +const provider_1 = __webpack_require__(344); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -43348,16 +43543,16 @@ exports.default = ProviderAsync; /***/ }), -/* 316 */ +/* 317 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const fsStat = __webpack_require__(317); -const fsWalk = __webpack_require__(322); -const reader_1 = __webpack_require__(342); +const fsStat = __webpack_require__(318); +const fsWalk = __webpack_require__(323); +const reader_1 = __webpack_require__(343); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -43410,15 +43605,15 @@ exports.default = ReaderStream; /***/ }), -/* 317 */ +/* 318 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(318); -const sync = __webpack_require__(319); -const settings_1 = __webpack_require__(320); +const async = __webpack_require__(319); +const sync = __webpack_require__(320); +const settings_1 = __webpack_require__(321); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43441,7 +43636,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 318 */ +/* 319 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43479,7 +43674,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 319 */ +/* 320 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43508,13 +43703,13 @@ exports.read = read; /***/ }), -/* 320 */ +/* 321 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(321); +const fs = __webpack_require__(322); class Settings { constructor(_options = {}) { this._options = _options; @@ -43531,7 +43726,7 @@ exports.default = Settings; /***/ }), -/* 321 */ +/* 322 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43554,16 +43749,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 322 */ +/* 323 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(323); -const stream_1 = __webpack_require__(338); -const sync_1 = __webpack_require__(339); -const settings_1 = __webpack_require__(341); +const async_1 = __webpack_require__(324); +const stream_1 = __webpack_require__(339); +const sync_1 = __webpack_require__(340); +const settings_1 = __webpack_require__(342); exports.Settings = settings_1.default; function walk(directory, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43593,13 +43788,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 323 */ +/* 324 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(324); +const async_1 = __webpack_require__(325); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -43630,17 +43825,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 324 */ +/* 325 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(156); -const fsScandir = __webpack_require__(325); -const fastq = __webpack_require__(334); -const common = __webpack_require__(336); -const reader_1 = __webpack_require__(337); +const fsScandir = __webpack_require__(326); +const fastq = __webpack_require__(335); +const common = __webpack_require__(337); +const reader_1 = __webpack_require__(338); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -43730,15 +43925,15 @@ exports.default = AsyncReader; /***/ }), -/* 325 */ +/* 326 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(326); -const sync = __webpack_require__(331); -const settings_1 = __webpack_require__(332); +const async = __webpack_require__(327); +const sync = __webpack_require__(332); +const settings_1 = __webpack_require__(333); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43761,16 +43956,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 326 */ +/* 327 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const rpl = __webpack_require__(327); -const constants_1 = __webpack_require__(328); -const utils = __webpack_require__(329); +const fsStat = __webpack_require__(318); +const rpl = __webpack_require__(328); +const constants_1 = __webpack_require__(329); +const utils = __webpack_require__(330); function read(directory, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings, callback); @@ -43858,7 +44053,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 327 */ +/* 328 */ /***/ (function(module, exports) { module.exports = runParallel @@ -43912,7 +44107,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 328 */ +/* 329 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43932,18 +44127,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = IS_MATCHED_BY_MAJOR || IS_MATCHED_B /***/ }), -/* 329 */ +/* 330 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(330); +const fs = __webpack_require__(331); exports.fs = fs; /***/ }), -/* 330 */ +/* 331 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43968,15 +44163,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 331 */ +/* 332 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const constants_1 = __webpack_require__(328); -const utils = __webpack_require__(329); +const fsStat = __webpack_require__(318); +const constants_1 = __webpack_require__(329); +const utils = __webpack_require__(330); function read(directory, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings); @@ -44027,15 +44222,15 @@ exports.readdir = readdir; /***/ }), -/* 332 */ +/* 333 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(317); -const fs = __webpack_require__(333); +const fsStat = __webpack_require__(318); +const fs = __webpack_require__(334); class Settings { constructor(_options = {}) { this._options = _options; @@ -44058,7 +44253,7 @@ exports.default = Settings; /***/ }), -/* 333 */ +/* 334 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44083,13 +44278,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 334 */ +/* 335 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(335) +var reusify = __webpack_require__(336) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -44263,7 +44458,7 @@ module.exports = fastqueue /***/ }), -/* 335 */ +/* 336 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44303,7 +44498,7 @@ module.exports = reusify /***/ }), -/* 336 */ +/* 337 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44334,13 +44529,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 337 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(336); +const common = __webpack_require__(337); class Reader { constructor(_root, _settings) { this._root = _root; @@ -44352,14 +44547,14 @@ exports.default = Reader; /***/ }), -/* 338 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const async_1 = __webpack_require__(324); +const async_1 = __webpack_require__(325); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -44389,13 +44584,13 @@ exports.default = StreamProvider; /***/ }), -/* 339 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(340); +const sync_1 = __webpack_require__(341); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -44410,15 +44605,15 @@ exports.default = SyncProvider; /***/ }), -/* 340 */ +/* 341 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(325); -const common = __webpack_require__(336); -const reader_1 = __webpack_require__(337); +const fsScandir = __webpack_require__(326); +const common = __webpack_require__(337); +const reader_1 = __webpack_require__(338); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -44476,14 +44671,14 @@ exports.default = SyncReader; /***/ }), -/* 341 */ +/* 342 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsScandir = __webpack_require__(325); +const fsScandir = __webpack_require__(326); class Settings { constructor(_options = {}) { this._options = _options; @@ -44509,15 +44704,15 @@ exports.default = Settings; /***/ }), -/* 342 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(317); -const utils = __webpack_require__(287); +const fsStat = __webpack_require__(318); +const utils = __webpack_require__(288); class Reader { constructor(_settings) { this._settings = _settings; @@ -44549,17 +44744,17 @@ exports.default = Reader; /***/ }), -/* 343 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(344); -const entry_1 = __webpack_require__(347); -const error_1 = __webpack_require__(348); -const entry_2 = __webpack_require__(349); +const deep_1 = __webpack_require__(345); +const entry_1 = __webpack_require__(348); +const error_1 = __webpack_require__(349); +const entry_2 = __webpack_require__(350); class Provider { constructor(_settings) { this._settings = _settings; @@ -44604,14 +44799,14 @@ exports.default = Provider; /***/ }), -/* 344 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); -const partial_1 = __webpack_require__(345); +const utils = __webpack_require__(288); +const partial_1 = __webpack_require__(346); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -44665,13 +44860,13 @@ exports.default = DeepFilter; /***/ }), -/* 345 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(346); +const matcher_1 = __webpack_require__(347); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -44710,13 +44905,13 @@ exports.default = PartialMatcher; /***/ }), -/* 346 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -44767,13 +44962,13 @@ exports.default = Matcher; /***/ }), -/* 347 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -44829,13 +45024,13 @@ exports.default = EntryFilter; /***/ }), -/* 348 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -44851,13 +45046,13 @@ exports.default = ErrorFilter; /***/ }), -/* 349 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -44884,15 +45079,15 @@ exports.default = EntryTransformer; /***/ }), -/* 350 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const stream_2 = __webpack_require__(316); -const provider_1 = __webpack_require__(343); +const stream_2 = __webpack_require__(317); +const provider_1 = __webpack_require__(344); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -44922,14 +45117,14 @@ exports.default = ProviderStream; /***/ }), -/* 351 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(352); -const provider_1 = __webpack_require__(343); +const sync_1 = __webpack_require__(353); +const provider_1 = __webpack_require__(344); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -44952,15 +45147,15 @@ exports.default = ProviderSync; /***/ }), -/* 352 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const fsWalk = __webpack_require__(322); -const reader_1 = __webpack_require__(342); +const fsStat = __webpack_require__(318); +const fsWalk = __webpack_require__(323); +const reader_1 = __webpack_require__(343); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -45002,7 +45197,7 @@ exports.default = ReaderSync; /***/ }), -/* 353 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45061,13 +45256,13 @@ exports.default = Settings; /***/ }), -/* 354 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(355); +const pathType = __webpack_require__(356); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -45143,7 +45338,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 355 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45193,7 +45388,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 356 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45201,9 +45396,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(285); -const gitIgnore = __webpack_require__(357); -const slash = __webpack_require__(358); +const fastGlob = __webpack_require__(286); +const gitIgnore = __webpack_require__(358); +const slash = __webpack_require__(359); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -45317,7 +45512,7 @@ module.exports.sync = options => { /***/ }), -/* 357 */ +/* 358 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -45920,7 +46115,7 @@ if ( /***/ }), -/* 358 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45938,7 +46133,7 @@ module.exports = path => { /***/ }), -/* 359 */ +/* 360 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45991,7 +46186,7 @@ module.exports = { /***/ }), -/* 360 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46013,7 +46208,7 @@ module.exports = path_ => { /***/ }), -/* 361 */ +/* 362 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46041,7 +46236,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 362 */ +/* 363 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(140) @@ -46407,12 +46602,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 363 */ +/* 364 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(364); +const AggregateError = __webpack_require__(365); module.exports = async ( iterable, @@ -46495,13 +46690,13 @@ module.exports = async ( /***/ }), -/* 364 */ +/* 365 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(365); -const cleanStack = __webpack_require__(366); +const indentString = __webpack_require__(366); +const cleanStack = __webpack_require__(367); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -46549,7 +46744,7 @@ module.exports = AggregateError; /***/ }), -/* 365 */ +/* 366 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46591,7 +46786,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 366 */ +/* 367 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46638,20 +46833,20 @@ module.exports = (stack, options) => { /***/ }), -/* 367 */ +/* 368 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(368); -const chalk = __webpack_require__(369); -const cliCursor = __webpack_require__(376); -const cliSpinners = __webpack_require__(380); -const logSymbols = __webpack_require__(382); -const stripAnsi = __webpack_require__(391); -const wcwidth = __webpack_require__(393); -const isInteractive = __webpack_require__(397); -const MuteStream = __webpack_require__(398); +const readline = __webpack_require__(369); +const chalk = __webpack_require__(370); +const cliCursor = __webpack_require__(377); +const cliSpinners = __webpack_require__(381); +const logSymbols = __webpack_require__(383); +const stripAnsi = __webpack_require__(392); +const wcwidth = __webpack_require__(394); +const isInteractive = __webpack_require__(398); +const MuteStream = __webpack_require__(399); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -47004,23 +47199,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 368 */ +/* 369 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 369 */ +/* 370 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(370); +const ansiStyles = __webpack_require__(371); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(374); +} = __webpack_require__(375); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -47221,7 +47416,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(375); + template = __webpack_require__(376); } return template(chalk, parts.join('')); @@ -47250,7 +47445,7 @@ module.exports = chalk; /***/ }), -/* 370 */ +/* 371 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47296,7 +47491,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(371); + colorConvert = __webpack_require__(372); } const offset = isBackground ? 10 : 0; @@ -47421,11 +47616,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 371 */ +/* 372 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(372); -const route = __webpack_require__(373); +const conversions = __webpack_require__(373); +const route = __webpack_require__(374); const convert = {}; @@ -47508,7 +47703,7 @@ module.exports = convert; /***/ }), -/* 372 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -48353,10 +48548,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 373 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(372); +const conversions = __webpack_require__(373); /* This function routes a model to all other models. @@ -48456,7 +48651,7 @@ module.exports = function (fromModel) { /***/ }), -/* 374 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48502,7 +48697,7 @@ module.exports = { /***/ }), -/* 375 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48643,12 +48838,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 376 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(377); +const restoreCursor = __webpack_require__(378); let isHidden = false; @@ -48685,12 +48880,12 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 377 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(378); +const onetime = __webpack_require__(379); const signalExit = __webpack_require__(218); module.exports = onetime(() => { @@ -48701,12 +48896,12 @@ module.exports = onetime(() => { /***/ }), -/* 378 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(379); +const mimicFn = __webpack_require__(380); const calledFunctions = new WeakMap(); @@ -48758,7 +48953,7 @@ module.exports.callCount = fn => { /***/ }), -/* 379 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48778,13 +48973,13 @@ module.exports.default = mimicFn; /***/ }), -/* 380 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(381)); +const spinners = Object.assign({}, __webpack_require__(382)); const spinnersList = Object.keys(spinners); @@ -48802,18 +48997,18 @@ module.exports.default = spinners; /***/ }), -/* 381 */ +/* 382 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]}}"); /***/ }), -/* 382 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(383); +const chalk = __webpack_require__(384); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -48835,16 +49030,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 383 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(179); -const ansiStyles = __webpack_require__(384); -const stdoutColor = __webpack_require__(389).stdout; +const ansiStyles = __webpack_require__(385); +const stdoutColor = __webpack_require__(390).stdout; -const template = __webpack_require__(390); +const template = __webpack_require__(391); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -49070,12 +49265,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 384 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(385); +const colorConvert = __webpack_require__(386); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -49243,11 +49438,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 385 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(386); -var route = __webpack_require__(388); +var conversions = __webpack_require__(387); +var route = __webpack_require__(389); var convert = {}; @@ -49327,11 +49522,11 @@ module.exports = convert; /***/ }), -/* 386 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(387); +var cssKeywords = __webpack_require__(388); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -50201,7 +50396,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 387 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50360,10 +50555,10 @@ module.exports = { /***/ }), -/* 388 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(386); +var conversions = __webpack_require__(387); /* this function routes a model to all other models. @@ -50463,7 +50658,7 @@ module.exports = function (fromModel) { /***/ }), -/* 389 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50601,7 +50796,7 @@ module.exports = { /***/ }), -/* 390 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50736,18 +50931,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 391 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(392); +const ansiRegex = __webpack_require__(393); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 392 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50764,14 +50959,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 393 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(394) -var combining = __webpack_require__(396) +var defaults = __webpack_require__(395) +var combining = __webpack_require__(397) var DEFAULTS = { nul: 0, @@ -50870,10 +51065,10 @@ function bisearch(ucs) { /***/ }), -/* 394 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(395); +var clone = __webpack_require__(396); module.exports = function(options, defaults) { options = options || {}; @@ -50888,7 +51083,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 395 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -51060,7 +51255,7 @@ if ( true && module.exports) { /***/ }), -/* 396 */ +/* 397 */ /***/ (function(module, exports) { module.exports = [ @@ -51116,7 +51311,7 @@ module.exports = [ /***/ }), -/* 397 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51132,7 +51327,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 398 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -51283,7 +51478,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 399 */ +/* 400 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51344,7 +51539,7 @@ const RunCommand = { }; /***/ }), -/* 400 */ +/* 401 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51354,7 +51549,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(144); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(145); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(146); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(401); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(402); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -51439,14 +51634,14 @@ const WatchCommand = { }; /***/ }), -/* 401 */ +/* 402 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(402); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -51513,141 +51708,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 402 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(403); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(404); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(404); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(405); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(405); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(406); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(406); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(410); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(411); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(420); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(424); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(424); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(425); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(425); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(426); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(105); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -51658,175 +51853,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["flatMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(467); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(468); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(477); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(481); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(481); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(482); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(482); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(483); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(489); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(490); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(490); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(491); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(491); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(492); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(492); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(493); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(493); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(494); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(494); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(495); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(495); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(496); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(496); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(497); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(497); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(498); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(499); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -51937,7 +52132,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 403 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52016,14 +52211,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 404 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(404); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -52039,7 +52234,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 405 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52086,7 +52281,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 406 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52187,7 +52382,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 407 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52348,7 +52543,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 408 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52467,7 +52662,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52560,7 +52755,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52620,7 +52815,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 411 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52636,7 +52831,7 @@ function combineAll(project) { /***/ }), -/* 412 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52668,7 +52863,7 @@ function combineLatest() { /***/ }), -/* 413 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52688,7 +52883,7 @@ function concat() { /***/ }), -/* 414 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52704,13 +52899,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 415 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(414); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(415); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -52720,7 +52915,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 416 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52785,7 +52980,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 417 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52870,7 +53065,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 418 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52946,7 +53141,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 419 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52996,7 +53191,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 420 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53004,7 +53199,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(421); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -53103,7 +53298,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 421 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53117,7 +53312,7 @@ function isDate(value) { /***/ }), -/* 422 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53263,7 +53458,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53301,7 +53496,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53377,7 +53572,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 425 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53448,13 +53643,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 426 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(425); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(426); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -53464,7 +53659,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 427 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53472,9 +53667,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(429); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(429); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(420); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(430); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -53496,7 +53691,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 428 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53562,7 +53757,7 @@ function defaultErrorFactory() { /***/ }), -/* 429 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53624,7 +53819,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 430 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53646,7 +53841,7 @@ function endWith() { /***/ }), -/* 431 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53708,7 +53903,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 432 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53762,7 +53957,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53856,7 +54051,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53968,7 +54163,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 435 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54006,7 +54201,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54078,13 +54273,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 437 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(437); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -54094,7 +54289,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 438 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54102,9 +54297,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(429); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(428); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(430); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(420); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(429); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -54121,7 +54316,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 439 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54158,7 +54353,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54202,7 +54397,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 441 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54210,9 +54405,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(442); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(443); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(429); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(420); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -54229,7 +54424,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 442 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54306,7 +54501,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54345,7 +54540,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54395,13 +54590,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 445 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -54414,15 +54609,15 @@ function max(comparer) { /***/ }), -/* 446 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(419); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(448); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(443); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(420); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -54443,7 +54638,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 447 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54525,7 +54720,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 448 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54545,7 +54740,7 @@ function merge() { /***/ }), -/* 449 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54570,7 +54765,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 450 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54679,13 +54874,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 451 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -54698,7 +54893,7 @@ function min(comparer) { /***/ }), -/* 452 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54747,7 +54942,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 453 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54837,7 +55032,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54885,7 +55080,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 455 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54908,7 +55103,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 456 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54948,14 +55143,14 @@ function plucker(props, length) { /***/ }), -/* 457 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -54968,14 +55163,14 @@ function publish(selector) { /***/ }), -/* 458 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -54986,14 +55181,14 @@ function publishBehavior(value) { /***/ }), -/* 459 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -55004,14 +55199,14 @@ function publishLast() { /***/ }), -/* 460 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -55027,7 +55222,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 461 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55054,7 +55249,7 @@ function race() { /***/ }), -/* 462 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55119,7 +55314,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 463 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55213,7 +55408,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 464 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55266,7 +55461,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 465 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55352,7 +55547,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 466 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55407,7 +55602,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 467 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55467,7 +55662,7 @@ function dispatchNotification(state) { /***/ }), -/* 468 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55590,13 +55785,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 469 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(453); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -55613,7 +55808,7 @@ function share() { /***/ }), -/* 470 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55682,7 +55877,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 471 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55762,7 +55957,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55804,7 +55999,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55866,7 +56061,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 474 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55923,7 +56118,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55979,7 +56174,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 476 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56008,13 +56203,13 @@ function startWith() { /***/ }), -/* 477 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(478); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(479); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -56039,7 +56234,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 478 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56103,13 +56298,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 479 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(480); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -56121,7 +56316,7 @@ function switchAll() { /***/ }), -/* 480 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56209,13 +56404,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 481 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(480); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -56225,7 +56420,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 482 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56273,7 +56468,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56341,7 +56536,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 484 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56429,7 +56624,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56531,7 +56726,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 486 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56540,7 +56735,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(485); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(486); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -56629,7 +56824,7 @@ function dispatchNext(arg) { /***/ }), -/* 487 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56637,7 +56832,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(447); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(448); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -56673,7 +56868,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 488 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56681,7 +56876,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(489); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(490); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -56698,7 +56893,7 @@ function timeout(due, scheduler) { /***/ }), -/* 489 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56706,7 +56901,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(421); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -56777,7 +56972,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 490 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56807,13 +57002,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 491 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -56830,7 +57025,7 @@ function toArray() { /***/ }), -/* 492 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56908,7 +57103,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 493 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56998,7 +57193,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 494 */ +/* 495 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57168,7 +57363,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 495 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57311,7 +57506,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 496 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57408,7 +57603,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57503,7 +57698,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 498 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57525,7 +57720,7 @@ function zip() { /***/ }), -/* 499 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57541,7 +57736,7 @@ function zipAll(project) { /***/ }), -/* 500 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57550,7 +57745,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(144); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(146); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(280); /* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(502); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -57632,159 +57827,6 @@ function toArray(value) { return Array.isArray(value) ? value : [value]; } -/***/ }), -/* 501 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "renderProjectsTree", function() { return renderProjectsTree; }); -/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(113); -/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - - -const projectKey = Symbol('__project'); -function renderProjectsTree(rootPath, projects) { - const projectsTree = buildProjectsTree(rootPath, projects); - return treeToString(createTreeStructure(projectsTree)); -} - -function treeToString(tree) { - return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); -} - -function childrenToStrings(tree, treePrefix) { - if (tree === undefined) { - return []; - } - - let strings = []; - tree.forEach((node, index) => { - const isLastNode = tree.length - 1 === index; - const nodePrefix = isLastNode ? '└── ' : '├── '; - const childPrefix = isLastNode ? ' ' : '│ '; - const childrenPrefix = treePrefix + childPrefix; - strings.push(`${treePrefix}${nodePrefix}${node.name}`); - strings = strings.concat(childrenToStrings(node.children, childrenPrefix)); - }); - return strings; -} - -function createTreeStructure(tree) { - let name; - const children = []; - - for (const [dir, project] of tree.entries()) { - // This is a leaf node (aka a project) - if (typeof project === 'string') { - name = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(project); - continue; - } // If there's only one project and the key indicates it's a leaf node, we - // know that we're at a package folder that contains a package.json, so we - // "inline it" so we don't get unnecessary levels, i.e. we'll just see - // `foo` instead of `foo -> foo`. - - - if (project.size === 1 && project.has(projectKey)) { - const projectName = project.get(projectKey); - children.push({ - children: [], - name: dirOrProjectName(dir, projectName) - }); - continue; - } - - const subtree = createTreeStructure(project); // If the name is specified, we know there's a package at the "root" of the - // subtree itself. - - if (subtree.name !== undefined) { - const projectName = subtree.name; - children.push({ - children: subtree.children, - name: dirOrProjectName(dir, projectName) - }); - continue; - } // Special-case whenever we have one child, so we don't get unnecessary - // folders in the output. E.g. instead of `foo -> bar -> baz` we get - // `foo/bar/baz` instead. - - - if (subtree.children && subtree.children.length === 1) { - const child = subtree.children[0]; - const newName = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(path__WEBPACK_IMPORTED_MODULE_1___default.a.join(dir.toString(), child.name)); - children.push({ - children: child.children, - name: newName - }); - continue; - } - - children.push({ - children: subtree.children, - name: chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(dir.toString()) - }); - } - - return { - name, - children - }; -} - -function dirOrProjectName(dir, projectName) { - return dir === projectName ? chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(dir) : chalk__WEBPACK_IMPORTED_MODULE_0___default.a`{dim ${dir.toString()} ({reset.green ${projectName}})}`; -} - -function buildProjectsTree(rootPath, projects) { - const tree = new Map(); - - for (const project of projects.values()) { - if (rootPath === project.path) { - tree.set(projectKey, project.name); - } else { - const relativeProjectPath = path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(rootPath, project.path); - addProjectToTree(tree, relativeProjectPath.split(path__WEBPACK_IMPORTED_MODULE_1___default.a.sep), project); - } - } - - return tree; -} - -function addProjectToTree(tree, pathParts, project) { - if (pathParts.length === 0) { - tree.set(projectKey, project.name); - } else { - const [currentDir, ...rest] = pathParts; - - if (!tree.has(currentDir)) { - tree.set(currentDir, new Map()); - } - - const subtree = tree.get(currentDir); - addProjectToTree(subtree, rest, project); - } -} - /***/ }), /* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -57796,7 +57838,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(503); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(361); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(362); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(275); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(146); @@ -58088,7 +58130,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(509); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -58239,7 +58281,7 @@ const os = __webpack_require__(121); const pAll = __webpack_require__(510); const arrify = __webpack_require__(512); const globby = __webpack_require__(513); -const isGlob = __webpack_require__(294); +const isGlob = __webpack_require__(295); const cpFile = __webpack_require__(713); const junk = __webpack_require__(723); const CpyError = __webpack_require__(724); @@ -58957,7 +58999,7 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); var globParent = __webpack_require__(521); -var isGlob = __webpack_require__(294); +var isGlob = __webpack_require__(295); var micromatch = __webpack_require__(524); var GLOBSTAR = '**'; /** @@ -59145,7 +59187,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(295); +var isExtglob = __webpack_require__(296); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -82800,7 +82842,7 @@ exports.flatten = flatten; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(284); +var merge2 = __webpack_require__(285); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 8ffd86b84bf76..944fcf5998637 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -4,6 +4,9 @@ "version": "1.0.0", "license": "Apache-2.0", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "build": "webpack", "kbn:watch": "webpack --watch --progress", diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 0fa3f355ae9d6..9a8048d6fd106 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -26,7 +26,7 @@ import { ICommand } from './'; import { getAllChecksums } from '../utils/project_checksums'; import { BootstrapCacheFile } from '../utils/bootstrap_cache_file'; import { readYarnLock } from '../utils/yarn_lock'; -import { validateYarnLock } from '../utils/validate_yarn_lock'; +import { validateDependencies } from '../utils/validate_dependencies'; export const BootstrapCommand: ICommand = { description: 'Install dependencies and crosslink projects', @@ -59,7 +59,7 @@ export const BootstrapCommand: ICommand = { const yarnLock = await readYarnLock(kbn); if (options.validate) { - await validateYarnLock(kbn, yarnLock); + await validateDependencies(kbn, yarnLock); } await linkProjectExecutables(projects, projectGraph); diff --git a/packages/kbn-pm/src/utils/project.ts b/packages/kbn-pm/src/utils/project.ts index 8f45df52c7a2f..4e4d76544aa35 100644 --- a/packages/kbn-pm/src/utils/project.ts +++ b/packages/kbn-pm/src/utils/project.ts @@ -153,6 +153,10 @@ export class Project { return (this.json.kibana && this.json.kibana.clean) || {}; } + public isFlaggedAsDevOnly() { + return !!(this.json.kibana && this.json.kibana.devOnly); + } + public hasScript(name: string) { return name in this.scripts; } diff --git a/packages/kbn-pm/src/utils/projects_tree.ts b/packages/kbn-pm/src/utils/projects_tree.ts index c7a13ce2de348..4ba000bc1b158 100644 --- a/packages/kbn-pm/src/utils/projects_tree.ts +++ b/packages/kbn-pm/src/utils/projects_tree.ts @@ -29,7 +29,7 @@ export function renderProjectsTree(rootPath: string, projects: Map {} -function treeToString(tree: ITree) { +export function treeToString(tree: ITree) { return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); } diff --git a/packages/kbn-pm/src/utils/validate_yarn_lock.ts b/packages/kbn-pm/src/utils/validate_dependencies.ts similarity index 77% rename from packages/kbn-pm/src/utils/validate_yarn_lock.ts rename to packages/kbn-pm/src/utils/validate_dependencies.ts index ec853a3a958fb..045d6332dcc29 100644 --- a/packages/kbn-pm/src/utils/validate_yarn_lock.ts +++ b/packages/kbn-pm/src/utils/validate_dependencies.ts @@ -20,14 +20,16 @@ // @ts-expect-error published types are useless import { stringify as stringifyLockfile } from '@yarnpkg/lockfile'; import dedent from 'dedent'; +import chalk from 'chalk'; import { writeFile } from './fs'; import { Kibana } from './kibana'; import { YarnLock } from './yarn_lock'; import { log } from './log'; import { Project } from './project'; +import { ITree, treeToString } from './projects_tree'; -export async function validateYarnLock(kbn: Kibana, yarnLock: YarnLock) { +export async function validateDependencies(kbn: Kibana, yarnLock: YarnLock) { // look through all of the packages in the yarn.lock file to see if // we have accidentally installed multiple lodash v4 versions const lodash4Versions = new Set(); @@ -157,5 +159,45 @@ export async function validateYarnLock(kbn: Kibana, yarnLock: YarnLock) { process.exit(1); } + // look for packages that have the the `kibana.devOnly` flag in their package.json + // and make sure they aren't included in the production dependencies of Kibana + const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(kbn, 'kibana'); + if (devOnlyProjectsInProduction) { + log.error(dedent` + Some of the packages in the production dependency chain for Kibana and X-Pack are + flagged with "kibana.devOnly" in their package.json. Please check changes made to + packages and their dependencies to ensure they don't end up in production. + + The devOnly dependencies that are being dependend on in production are: + + ${treeToString(devOnlyProjectsInProduction).split('\n').join('\n ')} + `); + + process.exit(1); + } + log.success('yarn.lock analysis completed without any issues'); } + +function getDevOnlyProductionDepsTree(kbn: Kibana, projectName: string) { + const project = kbn.getProject(projectName); + const childProjectNames = [ + ...Object.keys(project.productionDependencies).filter((name) => kbn.hasProject(name)), + ...(projectName === 'kibana' ? ['x-pack'] : []), + ]; + + const children = childProjectNames + .map((n) => getDevOnlyProductionDepsTree(kbn, n)) + .filter((t): t is ITree => !!t); + + if (!children.length && !project.isFlaggedAsDevOnly()) { + return; + } + + const tree: ITree = { + name: project.isFlaggedAsDevOnly() ? chalk.red.bold(projectName) : projectName, + children, + }; + + return tree; +} diff --git a/packages/kbn-release-notes/package.json b/packages/kbn-release-notes/package.json index 268530c22399a..e3306b7a54917 100644 --- a/packages/kbn-release-notes/package.json +++ b/packages/kbn-release-notes/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "Apache-2.0", "main": "target/index.js", + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "tsc", "kbn:watch": "tsc --watch" diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index a931dd3f3154d..8a5e885c456cd 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -9,12 +9,12 @@ "build": "tsc", "kbn:bootstrap": "yarn build" }, + "dependencies": { + "lodash": "^4.17.20" + }, "devDependencies": { + "@kbn/utility-types": "1.0.0", "typescript": "4.0.2", "tsd": "^0.13.1" - }, - "dependencies": { - "@kbn/utility-types": "1.0.0", - "lodash": "^4.17.20" } } diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 58359159e950d..5c57f6893d0c8 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -4,6 +4,9 @@ "private": true, "license": "Apache-2.0", "main": "./target/index.js", + "kibana": { + "devOnly": true + }, "dependencies": { "@kbn/dev-utils": "1.0.0", "@storybook/addon-actions": "^6.0.16", diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 4318cbcf2ec4e..cda2998901d56 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -4,6 +4,9 @@ "license": "Apache-2.0", "main": "./target/index.js", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "build": "babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline", "kbn:bootstrap": "yarn build", diff --git a/packages/kbn-test-subj-selector/package.json b/packages/kbn-test-subj-selector/package.json index 82a26dc4807be..b823c68f9560b 100755 --- a/packages/kbn-test-subj-selector/package.json +++ b/packages/kbn-test-subj-selector/package.json @@ -5,5 +5,8 @@ "main": "index.js", "keywords": [], "author": "Spencer Alger ", - "license": "Apache-2.0" + "license": "Apache-2.0", + "kibana": { + "devOnly": true + } } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 8422c34c9ed08..24096a41a5fdd 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -9,6 +9,9 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, + "kibana": { + "devOnly": true + }, "devDependencies": { "@babel/cli": "^7.10.5", "@jest/types": "^26.5.2", diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index d1d7a1c0397cf..6b531efcebace 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -5,6 +5,9 @@ "license": "Apache-2.0", "main": "target", "types": "target/index.d.ts", + "kibana": { + "devOnly": true + }, "scripts": { "build": "tsc", "kbn:bootstrap": "tsc", From d10d70b2ac67a9aa470bde0405192daacff277cf Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 15 Oct 2020 13:02:20 -0400 Subject: [PATCH 15/79] [Ingest Manager] Better validation of registry urls (#80685) --- x-pack/plugins/ingest_manager/server/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index e13c023d0d11a..a8b986be048ae 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -30,8 +30,8 @@ export const config: PluginConfigDescriptor = { ], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - registryUrl: schema.maybe(schema.uri()), - registryProxyUrl: schema.maybe(schema.uri()), + registryUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), From 41db7d175c282a14d038b3718ad0150bd96cc1a3 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 15 Oct 2020 13:24:17 -0400 Subject: [PATCH 16/79] [SECURITY SOLUTION] bug styling (#80572) * remove beta * fix i18n * fix cypress tests * forget to save * x-pack test --- .../alerts_detection_rules_custom.spec.ts | 4 ++-- .../alerts_detection_rules_eql.spec.ts | 2 +- .../alerts_detection_rules_ml.spec.ts | 2 +- .../alerts_detection_rules_override.spec.ts | 2 +- .../alerts_detection_rules_threshold.spec.ts | 2 +- .../cypress/integration/cases.spec.ts | 2 +- .../components/case_header_page/index.tsx | 9 -------- .../case_header_page/translations.ts | 22 ------------------- .../public/common/store/sourcerer/model.ts | 2 +- .../detection_engine_header_page/index.tsx | 9 -------- .../translations.ts | 22 ------------------- .../pages/endpoint_hosts/view/index.tsx | 2 +- .../pages/policy/view/policy_list.tsx | 2 +- .../trusted_apps/view/trusted_apps_page.tsx | 2 +- .../translations/translations/ja-JP.json | 4 ---- .../translations/translations/zh-CN.json | 4 ---- .../apps/endpoint/endpoint_list.ts | 2 +- .../apps/endpoint/policy_list.ts | 2 +- .../apps/endpoint/trusted_apps_list.ts | 2 +- 19 files changed, 14 insertions(+), 84 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 28889920e00e5..41665cf6d20a4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -168,7 +168,7 @@ describe('Custom detection rules creation', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity); @@ -328,7 +328,7 @@ describe('Custom detection rules deletion and edition', () => { fillAboutRule(editedRule); saveEditedRule(); - cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index 252ffb6c8c660..5502f35d6f0f8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -131,7 +131,7 @@ describe.skip('Detection rules, EQL', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 49ec6381cbc89..0f34e7d71e5fa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -115,7 +115,7 @@ describe('Detection rules, machine learning', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index abc873f2df0ee..edf7305f6916e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -132,7 +132,7 @@ describe('Detection rules, override', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index 9d988a46662fa..5095e856e3f65 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -129,7 +129,7 @@ describe('Detection rules, threshold', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index a45b1fd18a4b6..ec3887ad72625 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -60,7 +60,7 @@ describe('Cases', () => { createNewCaseWithTimeline(case1); backToCases(); - cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); + cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases'); cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0'); cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)'); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx index 4f7b17a730b6a..5e4db16d6d9cb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx @@ -7,18 +7,9 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -import * as i18n from './translations'; const CaseHeaderPageComponent: React.FC = (props) => ( ); -CaseHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts deleted file mode 100644 index 8cdc287b1584c..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_BADGE_LABEL = i18n.translate( - 'xpack.securitySolution.case.caseView.pageBadgeLabel', - { - defaultMessage: 'Beta', - } -); - -export const PAGE_BADGE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.case.caseView.pageBadgeTooltip', - { - defaultMessage: - 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', - } -); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts index 93f7ff95dfb00..18aa4e65a03cf 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts @@ -56,7 +56,7 @@ export const initSourcererScope = { errorMessage: null, indexPattern: EMPTY_INDEX_PATTERN, indicesExist: true, - loading: true, + loading: false, selectedPatterns: [], }; diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx index 1a2deb059ad4f..293ed4d488c7d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx @@ -7,20 +7,11 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -import * as i18n from './translations'; const DetectionEngineHeaderPageComponent: React.FC = (props) => ( ); -DetectionEngineHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); DetectionEngineHeaderPage.displayName = 'DetectionEngineHeaderPage'; diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts deleted file mode 100644 index f59be16923805..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_BADGE_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel', - { - defaultMessage: 'Beta', - } -); - -export const PAGE_BADGE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip', - { - defaultMessage: - 'Alerts is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', - } -); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 36c5b0d1037e5..c5d3c3c25313d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -561,7 +561,7 @@ export const EndpointList = () => { return ( { )} { return ( { it('finds page title', async () => { const title = await testSubjects.getVisibleText('header-page-title'); - expect(title).to.equal('Endpoints BETA'); + expect(title).to.equal('Endpoints'); }); it('displays table data', async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 49a7a2155a700..70958d7ca7631 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('displays page title', async () => { const policyTitle = await testSubjects.getVisibleText('header-page-title'); - expect(policyTitle).to.equal('Policies BETA'); + expect(policyTitle).to.equal('Policies'); }); it('shows header create policy button', async () => { const createButtonTitle = await testSubjects.getVisibleText('headerCreateNewPolicyButton'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 78ef1bc894e0b..3a0f0b91bddb3 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -20,7 +20,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should show page title', async () => { expect(await testSubjects.getVisibleText('header-page-title')).to.equal( - 'Trusted Applications BETA' + 'Trusted Applications' ); }); From 6b8e8a5b468746f14beb6e17264e5b9c5bebc67b Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Thu, 15 Oct 2020 18:46:11 +0100 Subject: [PATCH 17/79] [Security Solution] Update button text according to status (#80389) * update button text according to status * remove unused translations * fix functional test * fixup * fix unit test * update unit tests * update unit test --- .../load_empty_prompt.test.tsx | 96 ++++++++++- .../pre_packaged_rules/load_empty_prompt.tsx | 42 +++-- .../rules/pre_packaged_rules/translations.ts | 7 - .../detection_engine/rules/translations.ts | 54 ++++++ .../rules/use_pre_packaged_rules.test.tsx | 12 ++ .../rules/use_pre_packaged_rules.tsx | 128 +++++++++++++- .../detection_engine/rules/index.test.tsx | 163 +++++++++++++++++- .../pages/detection_engine/rules/index.tsx | 68 +++----- .../detection_engine/rules/translations.ts | 40 ----- .../translations/translations/ja-JP.json | 3 +- .../translations/translations/zh-CN.json | 3 +- 11 files changed, 492 insertions(+), 124 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index a41da908085bc..75ab1524c5c06 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { waitFor } from '@testing-library/react'; +import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { PrePackagedRulesPrompt } from './load_empty_prompt'; +import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -23,16 +25,94 @@ jest.mock('react-router-dom', () => { jest.mock('../../../../common/components/link_to'); +jest.mock('../../../containers/detection_engine/rules/api', () => ({ + getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }), + createPrepackagedRules: jest.fn(), +})); + +const props = { + createPrePackagedRules: jest.fn(), + loading: false, + userHasNoPermissions: false, + 'data-test-subj': 'load-prebuilt-rules', +}; + describe('PrePackagedRulesPrompt', () => { it('renders correctly', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('EmptyPrompt')).toHaveLength(1); }); }); + +describe('LoadPrebuiltRulesAndTemplatesButton', () => { + it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt rules and timeline templates' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt rules' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt timeline templates' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 99968cd4d9fe8..64b3cfa3aa991 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; @@ -14,6 +14,8 @@ import * as i18n from './translations'; import { LinkButton } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; import { useFormatUrl } from '../../../../common/components/link_to'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; +import { useUserData } from '../../user_info'; const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ @@ -46,24 +48,36 @@ const PrePackagedRulesPromptComponent: React.FC = ( [history] ); + const [ + { isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }, + ] = useUserData(); + + const { getLoadPrebuiltRulesAndTemplatesButton } = usePrePackagedRules({ + canUserCRUD, + hasIndexWrite, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + }); + + const loadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions, + onClick: handlePreBuiltCreation, + fill: true, + 'data-test-subj': 'load-prebuilt-rules', + }), + [getLoadPrebuiltRulesAndTemplatesButton, handlePreBuiltCreation, userHasNoPermissions] + ); + return ( {i18n.PRE_BUILT_TITLE}} body={

{i18n.PRE_BUILT_MSG}

} actions={ - - - {i18n.PRE_BUILT_ACTION} - - + {loadPrebuiltRulesAndTemplatesButton} + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', + { + values: { missingRules }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', + { + values: { missingTimelines }, + defaultMessage: + 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( + missingRules: number, + missingTimelines: number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', + { + values: { missingRules, missingTimelines }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 92d46a785b034..7f74e92584494 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -32,6 +32,10 @@ describe('usePrePackagedRules', () => { await waitForNextUpdate(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: null, loading: true, loadingCreatePrePackagedRules: false, @@ -63,6 +67,10 @@ describe('usePrePackagedRules', () => { await waitForNextUpdate(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: result.current.createPrePackagedRules, loading: false, loadingCreatePrePackagedRules: false, @@ -100,6 +108,10 @@ describe('usePrePackagedRules', () => { expect(resp).toEqual(true); expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: result.current.createPrePackagedRules, loading: false, loadingCreatePrePackagedRules: false, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index d82d97883a3d0..4d19f44bcfc84 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; +import { EuiButton } from '@elastic/eui'; import { errorToToaster, @@ -14,6 +15,11 @@ import { import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; import * as i18n from './translations'; +import { + getPrePackagedRuleStatus, + getPrePackagedTimelineStatus, +} from '../../../pages/detection_engine/rules/helpers'; + type Func = () => void; export type CreatePreBuiltRules = () => Promise; @@ -23,6 +29,23 @@ interface ReturnPrePackagedTimelines { timelinesNotUpdated: number | null; } +type GetLoadPrebuiltRulesAndTemplatesButton = (args: { + isDisabled: boolean; + onClick: () => void; + fill?: boolean; + 'data-test-subj'?: string; +}) => React.ReactNode | null; + +type GetReloadPrebuiltRulesAndTemplatesButton = ({ + isDisabled, + onClick, + fill, +}: { + isDisabled: boolean; + onClick: () => void; + fill?: boolean; +}) => React.ReactNode | null; + interface ReturnPrePackagedRules { createPrePackagedRules: null | CreatePreBuiltRules; loading: boolean; @@ -32,6 +55,8 @@ interface ReturnPrePackagedRules { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; + getLoadPrebuiltRulesAndTemplatesButton: GetLoadPrebuiltRulesAndTemplatesButton; + getReloadPrebuiltRulesAndTemplatesButton: GetReloadPrebuiltRulesAndTemplatesButton; } export type ReturnPrePackagedRulesAndTimelines = ReturnPrePackagedRules & @@ -89,7 +114,6 @@ export const usePrePackagedRules = ({ const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); @@ -100,7 +124,6 @@ export const usePrePackagedRules = ({ const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({ signal: abortCtrl.signal, }); - if (isSubscribed) { setPrepackagedDataStatus({ createPrePackagedRules: createElasticRules, @@ -225,9 +248,108 @@ export const usePrePackagedRules = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]); + const prePackagedRuleStatus = useMemo( + () => + getPrePackagedRuleStatus( + prepackagedDataStatus.rulesInstalled, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.rulesNotUpdated + ), + [ + prepackagedDataStatus.rulesInstalled, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.rulesNotUpdated, + ] + ); + + const prePackagedTimelineStatus = useMemo( + () => + getPrePackagedTimelineStatus( + prepackagedDataStatus.timelinesInstalled, + prepackagedDataStatus.timelinesNotInstalled, + prepackagedDataStatus.timelinesNotUpdated + ), + [ + prepackagedDataStatus.timelinesInstalled, + prepackagedDataStatus.timelinesNotInstalled, + prepackagedDataStatus.timelinesNotUpdated, + ] + ); + const getLoadPrebuiltRulesAndTemplatesButton = useCallback( + ({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => { + return prePackagedRuleStatus === 'ruleNotInstalled' || + prePackagedTimelineStatus === 'timelinesNotInstalled' ? ( + + {prePackagedRuleStatus === 'ruleNotInstalled' && + prePackagedTimelineStatus === 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES} + + {prePackagedRuleStatus === 'ruleNotInstalled' && + prePackagedTimelineStatus !== 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_RULES} + + {prePackagedRuleStatus !== 'ruleNotInstalled' && + prePackagedTimelineStatus === 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES} + + ) : null; + }, + [loadingCreatePrePackagedRules, prePackagedRuleStatus, prePackagedTimelineStatus] + ); + + const getMissingRulesOrTimelinesButtonTitle = useCallback( + (missingRules: number, missingTimelines: number) => { + if (missingRules > 0 && missingTimelines === 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); + else if (missingRules === 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); + else if (missingRules > 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); + }, + [] + ); + + const getReloadPrebuiltRulesAndTemplatesButton = useCallback( + ({ isDisabled, onClick, fill = false }) => { + return prePackagedRuleStatus === 'someRuleUninstall' || + prePackagedTimelineStatus === 'someTimelineUninstall' ? ( + + {getMissingRulesOrTimelinesButtonTitle( + prepackagedDataStatus.rulesNotInstalled ?? 0, + prepackagedDataStatus.timelinesNotInstalled ?? 0 + )} + + ) : null; + }, + [ + getMissingRulesOrTimelinesButtonTitle, + loadingCreatePrePackagedRules, + prePackagedRuleStatus, + prePackagedTimelineStatus, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.timelinesNotInstalled, + ] + ); + return { loading, loadingCreatePrePackagedRules, ...prepackagedDataStatus, + getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 886a24dd7cbe8..58e61c5b47486 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { RulesPage } from './index'; import { useUserData } from '../../../components/user_info'; -import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; - +import { waitFor } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -26,16 +27,164 @@ jest.mock('react-router-dom', () => { jest.mock('../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); -jest.mock('../../../containers/detection_engine/rules'); +jest.mock('../../../../common/components/toasters', () => { + const actual = jest.requireActual('../../../../common/components/toasters'); + return { + ...actual, + errorToToaster: jest.fn(), + useStateToaster: jest.fn().mockReturnValue([jest.fn(), jest.fn()]), + displaySuccessToast: jest.fn(), + }; +}); + +jest.mock('../../../containers/detection_engine/rules/api', () => ({ + getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }), + createPrepackagedRules: jest.fn(), +})); + +jest.mock('../../../../common/lib/kibana', () => { + return { + useToast: jest.fn(), + useHttp: jest.fn(), + }; +}); + +jest.mock('../../../components/value_lists_management_modal', () => { + return { + ValueListsModal: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('./all', () => { + return { + AllRules: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('../../../../common/utils/route/spy_routes', () => { + return { + SpyRoute: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('../../../components/rules/pre_packaged_rules/update_callout', () => { + return { + UpdatePrePackagedRulesCallOut: jest.fn().mockReturnValue(
), + }; +}); describe('RulesPage', () => { beforeAll(() => { (useUserData as jest.Mock).mockReturnValue([{}]); - (usePrePackagedRules as jest.Mock).mockReturnValue({}); }); - it('renders correctly', () => { + + it('renders AllRules', () => { const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="all-rules"]').exists()).toEqual(true); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt rules and timeline templates' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt rules' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt timeline templates' + ); + }); + }); + + it('renders a callout - Update Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 2, + rules_installed: 1, + rules_not_updated: 1, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); - expect(wrapper.find('AllRules')).toHaveLength(1); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="update-callout-button"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 53c82569f94ae..8c7cb6a0d9284 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; @@ -70,6 +70,8 @@ const RulesPageComponent: React.FC = () => { timelinesInstalled, timelinesNotInstalled, timelinesNotUpdated, + getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton, } = usePrePackagedRules({ canUserCRUD, hasIndexWrite, @@ -113,18 +115,6 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); - const getMissingRulesOrTimelinesButtonTitle = useCallback( - (missingRules: number, missingTimelines: number) => { - if (missingRules > 0 && missingTimelines === 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); - else if (missingRules === 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); - else if (missingRules > 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); - }, - [] - ); - const goToNewRule = useCallback( (ev) => { ev.preventDefault(); @@ -133,6 +123,24 @@ const RulesPageComponent: React.FC = () => { [history] ); + const loadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions(canUserCRUD) || loading, + onClick: handleCreatePrePackagedRules, + }), + [canUserCRUD, getLoadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + ); + + const reloadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions(canUserCRUD) || loading, + onClick: handleCreatePrePackagedRules, + }), + [canUserCRUD, getReloadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + ); + if ( redirectToDetections( isSignalIndexExists, @@ -177,35 +185,11 @@ const RulesPageComponent: React.FC = () => { title={i18n.PAGE_TITLE} > - {(prePackagedRuleStatus === 'ruleNotInstalled' || - prePackagedTimelineStatus === 'timelinesNotInstalled') && ( - - - {i18n.LOAD_PREPACKAGED_RULES} - - + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} )} - {(prePackagedRuleStatus === 'someRuleUninstall' || - prePackagedTimelineStatus === 'someTimelineUninstall') && ( - - - {getMissingRulesOrTimelinesButtonTitle( - rulesNotInstalled ?? 0, - timelinesNotInstalled ?? 0 - )} - - + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} )} @@ -247,6 +231,7 @@ const RulesPageComponent: React.FC = () => { {(prePackagedRuleStatus === 'ruleNeedUpdate' || prePackagedTimelineStatus === 'timelineNeedUpdate') && ( { )} - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', - { - values: { missingRules }, - defaultMessage: - 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', - } - ); - -export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', - { - values: { missingTimelines }, - defaultMessage: - 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', - } - ); - -export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( - missingRules: number, - missingTimelines: number -) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', - { - values: { missingRules, missingTimelines }, - defaultMessage: - 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', - } - ); - export const IMPORT_RULE_BTN_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle', { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 763b26de00940..d0c24b0239b62 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15574,13 +15574,12 @@ "xpack.securitySolution.detectionEngine.rules.deleteDescription": "削除", "xpack.securitySolution.detectionEngine.rules.editPageTitle": "編集", "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "ルールのインポート...", - "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む", + "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "オプション", "xpack.securitySolution.detectionEngine.rules.pageTitle": "検出ルール", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "独自のルールの作成", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、Elastic Endpoint Securityルールを除くすべての事前構築済みルールが無効になっています。有効にする追加のルールを選択することができます。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "Elastic事前構築済み検出ルールを読み込む", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "事前構築済み検出ルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "リリースノート", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "{missingRules} Elastic事前構築済み{missingRules, plural, =1 {ルール} other {ルール}}と{missingTimelines} Elastic事前構築済み{missingTimelines, plural, =1 {タイムライン} other {タイムライン}}をインストール ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "{missingRules} Elasticの事前構築済みの{missingRules, plural, =1 {個のルール} other {個のルール}}をインストール ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4245b446c5b93..db73cd8043e7e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15583,13 +15583,12 @@ "xpack.securitySolution.detectionEngine.rules.deleteDescription": "删除", "xpack.securitySolution.detectionEngine.rules.editPageTitle": "编辑", "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "导入规则……", - "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "加载 Elastic 预构建规则和时间线模板", + "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton": "加载 Elastic 预构建规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "可选", "xpack.securitySolution.detectionEngine.rules.pageTitle": "检测规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "创建自己的规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件满足时创建告警。默认情况下,除 Elastic Endpoint Security 规则外,所有预置规则都处于禁用状态。您可以选择其他要激活的规则。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "加载 Elastic 预构建检测规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预置检测规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "发行说明", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}}以及 {missingTimelines} 个 Elastic 预构建{missingTimelines, plural, =1 {时间线} other {时间线}} ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}} ", From b1af4ba9ae74188306f22d00c7e36ecce8886772 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 15 Oct 2020 13:58:54 -0400 Subject: [PATCH 18/79] [Maps] Add support for envelope (#80614) --- .../elasticsearch_geo_utils.js | 10 ++++++++ .../elasticsearch_geo_utils.test.js | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js index be214e3b01e67..813d01ff90861 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js @@ -175,6 +175,16 @@ export function convertESShapeToGeojsonGeometry(value) { geoJson.type = GEO_JSON_TYPE.GEOMETRY_COLLECTION; break; case 'envelope': + // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope + const polygon = formatEnvelopeAsPolygon({ + minLon: geoJson.coordinates[0][0], + maxLon: geoJson.coordinates[1][0], + minLat: geoJson.coordinates[1][1], + maxLat: geoJson.coordinates[0][1], + }); + geoJson.type = polygon.type; + geoJson.coordinates = polygon.coordinates; + break; case 'circle': const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage', diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index a8d5d650740cd..ccab57dd18339 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -250,6 +250,30 @@ describe('geoShapeToGeometry', () => { expect(shapes[0].coordinates).toEqual(coordinates); }); + it('Should convert envelope to geojson', () => { + const coordinates = [ + [100.0, 1.0], + [101.0, 0.0], + ]; + const value = { + type: 'envelope', + coordinates: coordinates, + }; + const shapes = []; + geoShapeToGeometry(value, shapes); + expect(shapes.length).toBe(1); + expect(shapes[0].type).toBe('Polygon'); + expect(shapes[0].coordinates).toEqual([ + [ + [100, 1], + [100, 0], + [101, 0], + [101, 1], + [100, 1], + ], + ]); + }); + it('Should convert array of values', () => { const linestringCoordinates = [ [-77.03653, 38.897676], From 5dc91229f290aeacffb3d4979234764b48eeb704 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 15 Oct 2020 14:20:54 -0400 Subject: [PATCH 19/79] Fix role mappings test for ESS (#80604) Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- test/common/services/security/role_mappings.ts | 18 ++++++++++++++++-- .../functional/apps/security/role_mappings.ts | 4 ++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index 7951d4b5b47b2..267294991f30e 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -23,10 +23,24 @@ import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class RoleMappings { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} + public async getAll() { + this.log.debug(`Getting role mappings`); + const { data, status, statusText } = await this.kbnClient.request>({ + path: `/internal/security/role_mapping`, + method: 'GET', + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + return data; + } + public async create(name: string, roleMapping: Record) { this.log.debug(`creating role mapping ${name}`); const { data, status, statusText } = await this.kbnClient.request({ - path: `/internal/security/role_mapping/${name}`, + path: `/internal/security/role_mapping/${encodeURIComponent(name)}`, method: 'POST', body: roleMapping, }); @@ -41,7 +55,7 @@ export class RoleMappings { public async delete(name: string) { this.log.debug(`deleting role mapping ${name}`); const { data, status, statusText } = await this.kbnClient.request({ - path: `/internal/security/role_mapping/${name}`, + path: `/internal/security/role_mapping/${encodeURIComponent(name)}`, method: 'DELETE', }); if (status !== 200 && status !== 404) { diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 60c166d837933..96f16aebd11b9 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -17,6 +17,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Role Mappings', function () { before(async () => { + // Delete any existing role mappings. ESS commonly sets up a role mapping automatically. + const existingMappings = await security.roleMappings.getAll(); + await Promise.all(existingMappings.map((m) => security.roleMappings.delete(m.name))); + await pageObjects.common.navigateToApp('roleMappings'); }); From 9d50c17fa683c49f3a89c2af9ac8d2c4eee64269 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 15 Oct 2020 20:45:11 +0200 Subject: [PATCH 20/79] [APM] Hide service if only data is from ML (#80145) Closes #79998. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_services/get_services_items.ts | 29 ++++++++++++++----- .../trial/tests/services/top_services.ts | 19 ++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 4cb0c4c750dd1..5ea3714e81b6f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -58,14 +58,27 @@ export async function getServicesItems({ }), ]); - const allMetrics = [ - ...transactionDurationAverages, - ...agentNames, - ...transactionRates, - ...transactionErrorRates, - ...environments, - ...healthStatuses, - ]; + const apmServiceMetrics = joinByKey( + [ + ...transactionDurationAverages, + ...agentNames, + ...transactionRates, + ...transactionErrorRates, + ...environments, + ], + 'serviceName' + ); + + const apmServices = apmServiceMetrics.map(({ serviceName }) => serviceName); + + // make sure to exclude health statuses from services + // that are not found in APM data + + const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => + apmServices.includes(serviceName) + ); + + const allMetrics = [...apmServiceMetrics, ...matchedHealthStatuses]; return joinByKey(allMetrics, 'serviceName'); } diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 6fd5e7e0c3ea7..9a6c6f94dbb60 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -99,6 +99,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(definedHealthStatuses.length).to.be(0); }); }); + + describe('and fetching a list of services with a filter', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${encodeURIComponent( + `{"kuery":"service.name:opbeans-java","environment":"ENVIRONMENT_ALL"}` + )}` + ); + }); + + it('does not return health statuses for services that are not found in APM data', () => { + expect(response.status).to.be(200); + + expect(response.body.items.length).to.be(1); + + expect(response.body.items[0].serviceName).to.be('opbeans-java'); + }); + }); }); }); } From 8d053fdc8eff158d35f96053d295936734c9535c Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Thu, 15 Oct 2020 19:46:20 +0100 Subject: [PATCH 21/79] [Security Solution] Cypress template creation (#80180) * init tests * fix cypress test * remove console * fix functional test * update functional test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../integration/timeline_creation.spec.ts | 3 +-- .../timeline_template_creation.spec.ts | 20 +++++++++---------- .../cypress/screens/timeline.ts | 5 +++++ .../cypress/tasks/timeline.ts | 11 ++++++++-- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts index 8ce60450671b9..9f61d11b7ac0f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -45,8 +45,7 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/79389 -describe.skip('Timelines', () => { +describe('Timelines', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts index 91255d6110d59..377b2100b36cd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts @@ -12,8 +12,8 @@ import { NOTES_BUTTON, NOTES_COUNT, NOTES_TEXT_AREA, + PIN_EVENT, TIMELINE_DESCRIPTION, - // TIMELINE_FILTER, TIMELINE_QUERY, TIMELINE_TITLE, } from '../screens/timeline'; @@ -35,7 +35,7 @@ import { closeTimeline, createNewTimelineTemplate, markAsFavorite, - openTimelineFromSettings, + openTimelineTemplateFromSettings, populateTimeline, waitForTimelineChanges, } from '../tasks/timeline'; @@ -43,8 +43,7 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/79967 -describe.skip('Timeline Templates', () => { +describe('Timeline Templates', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); @@ -56,12 +55,11 @@ describe.skip('Timeline Templates', () => { createNewTimelineTemplate(); populateTimeline(); addFilter(timeline.filter); - // To fix - // cy.get(PIN_EVENT).should( - // 'have.attr', - // 'aria-label', - // 'This event may not be pinned while editing a template timeline' - // ); + cy.get(PIN_EVENT).should( + 'have.attr', + 'aria-label', + 'This event may not be pinned while editing a template timeline' + ); cy.get(LOCKED_ICON).should('be.visible'); addNameToTimeline(timeline.title); @@ -77,7 +75,7 @@ describe.skip('Timeline Templates', () => { waitForTimelineChanges(); createNewTimelineTemplate(); closeTimeline(); - openTimelineFromSettings(); + openTimelineTemplateFromSettings(timelineId); cy.contains(timeline.title).should('exist'); cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index e397dd9b5a41a..98e6502ffe94f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -58,6 +58,9 @@ export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; +export const OPEN_TIMELINE_TEMPLATE_ICON = + '[data-test-subj="open-timeline-modal-body-filter-template"]'; + export const PIN_EVENT = '[data-test-subj="pin"]'; export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; @@ -98,6 +101,8 @@ export const TIMELINE_FILTER = (filter: TimelineFilter) => { export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; +export const TIMELINE_TITLE_BY_ID = (id: string) => `[data-test-subj="title-${id}"]`; + export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]'; export const TIMELINE_FILTER_VALUE = diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 7c9c95427a4d0..b101793385488 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -41,9 +41,11 @@ import { TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, + TIMELINE_TITLE_BY_ID, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, CREATE_NEW_TIMELINE_TEMPLATE, + OPEN_TIMELINE_TEMPLATE_ICON, } from '../screens/timeline'; import { TIMELINES_TABLE } from '../screens/timelines'; @@ -69,8 +71,7 @@ export const addNotesToTimeline = (notes: string) => { export const addFilter = (filter: TimelineFilter) => { cy.get(ADD_FILTER).click(); - cy.get(TIMELINE_FILTER_FIELD).type(filter.field); - cy.get(COMBO_BOX).contains(filter.field).click(); + cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`); cy.get(TIMELINE_FILTER_OPERATOR).type(filter.operator); cy.get(COMBO_BOX).contains(filter.operator).click(); if (filter.operator !== 'exists') { @@ -146,6 +147,12 @@ export const openTimelineFromSettings = () => { cy.get(OPEN_TIMELINE_ICON).click({ force: true }); }; +export const openTimelineTemplateFromSettings = (id: string) => { + openTimelineFromSettings(); + cy.get(OPEN_TIMELINE_TEMPLATE_ICON).click({ force: true }); + cy.get(TIMELINE_TITLE_BY_ID(id)).click({ force: true }); +}; + export const openTimelineSettings = () => { cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); }; From 3ad698d6a0293adb7f56bf570f05c08e2aa59548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 15 Oct 2020 14:47:46 -0400 Subject: [PATCH 22/79] Licensed feature usage for connectors (#77679) * Initial work * Fix type check and jest failures * Add unit tests * No need to notifyUsage from alert execution handler * Fix ESLint * Log action usage from alerts * Add integration tests * Fix jest test * Skip feature usage of basic action types * Fix types * Fix ESLint issue * Clarify comment Co-authored-by: Elastic Machine --- .../server/action_type_registry.test.ts | 80 ++++++++++++++++++- .../actions/server/action_type_registry.ts | 38 +++++++-- .../actions/server/actions_client.mock.ts | 1 + .../actions/server/actions_client.test.ts | 33 +++++++- .../plugins/actions/server/actions_client.ts | 7 ++ .../server/builtin_action_types/index.test.ts | 2 + .../server/create_execute_function.test.ts | 6 +- .../actions/server/create_execute_function.ts | 2 +- .../server/lib/action_executor.test.ts | 3 + .../actions/server/lib/action_executor.ts | 2 +- .../lib/get_action_type_feature_usage_name.ts | 11 +++ x-pack/plugins/actions/server/lib/index.ts | 1 + .../actions/server/lib/license_state.mock.ts | 1 + .../actions/server/lib/license_state.test.ts | 43 ++++++++++ .../actions/server/lib/license_state.ts | 23 +++++- x-pack/plugins/actions/server/plugin.test.ts | 45 +++++++++++ x-pack/plugins/actions/server/plugin.ts | 27 +++++-- .../server/alerts_client/alerts_client.ts | 5 ++ .../server/alerts_client/tests/create.test.ts | 14 +++- .../server/alerts_client/tests/update.test.ts | 7 +- .../task_runner/create_execution_handler.ts | 4 +- .../spaces_only/tests/actions/create.ts | 26 ++++++ .../spaces_only/tests/actions/execute.ts | 34 ++++++++ .../spaces_only/tests/actions/update.ts | 37 +++++++++ .../spaces_only/tests/alerting/alerts_base.ts | 25 ++++++ 25 files changed, 451 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index b25e33400df5d..e641b81189b93 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -11,6 +11,7 @@ import { ActionExecutor, ExecutorError, ILicenseState, TaskRunnerFactory } from import { actionsConfigMock } from './actions_config.mock'; import { licenseStateMock } from './lib/license_state.mock'; import { ActionsConfigurationUtilities } from './actions_config'; +import { licensingMock } from '../../licensing/server/mocks'; const mockTaskManager = taskManagerMock.setup(); let mockedLicenseState: jest.Mocked; @@ -22,6 +23,7 @@ beforeEach(() => { mockedLicenseState = licenseStateMock.create(); mockedActionsConfig = actionsConfigMock.create(); actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -51,7 +53,7 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - minimumLicenseRequired: 'basic', + minimumLicenseRequired: 'gold', executor, }); expect(actionTypeRegistry.has('my-action-type')).toEqual(true); @@ -69,6 +71,10 @@ describe('register()', () => { }, ] `); + expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith( + 'Connector: My action type', + 'gold' + ); }); test('shallow clones the given action type', () => { @@ -123,6 +129,31 @@ describe('register()', () => { expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false); expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime); }); + + test('registers gold+ action types to the licensing feature usage API', () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor, + }); + expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith( + 'Connector: My action type', + 'gold' + ); + }); + + test(`doesn't register basic action types to the licensing feature usage API`, () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + expect(actionTypeRegistryParams.licensing.featureUsage.register).not.toHaveBeenCalled(); + }); }); describe('get()', () => { @@ -232,10 +263,20 @@ describe('isActionTypeEnabled', () => { expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); }); - test('should call isLicenseValidForActionType of the license state', async () => { + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); - expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); }); test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true', async () => { @@ -298,3 +339,36 @@ describe('ensureActionTypeEnabled', () => { ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); }); + +describe('isActionExecutable()', () => { + let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, + }; + + beforeEach(() => { + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(fooActionType); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('123', 'foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('123', 'foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 4015381ff9502..b93d4a6e78ac6 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -7,9 +7,15 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; -import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { + ExecutorError, + getActionTypeFeatureUsageName, + TaskRunnerFactory, + ILicenseState, +} from './lib'; import { ActionType, PreConfiguredAction, @@ -19,6 +25,7 @@ import { } from './types'; export interface ActionTypeRegistryOpts { + licensing: LicensingPluginSetup; taskManager: TaskManagerSetupContract; taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; @@ -33,6 +40,7 @@ export class ActionTypeRegistry { private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly licensing: LicensingPluginSetup; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; @@ -40,6 +48,7 @@ export class ActionTypeRegistry { this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; this.preconfiguredActions = constructorParams.preconfiguredActions; + this.licensing = constructorParams.licensing; } /** @@ -54,26 +63,36 @@ export class ActionTypeRegistry { */ public ensureActionTypeEnabled(id: string) { this.actionsConfigUtils.ensureActionTypeEnabled(id); + // Important to happen last because the function will notify of feature usage at the + // same time and it shouldn't notify when the action type isn't enabled this.licenseState.ensureLicenseForActionType(this.get(id)); } /** * Returns true if action type is enabled in the config and a valid license is used. */ - public isActionTypeEnabled(id: string) { + public isActionTypeEnabled( + id: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { return ( this.actionsConfigUtils.isActionTypeEnabled(id) && - this.licenseState.isLicenseValidForActionType(this.get(id)).isValid === true + this.licenseState.isLicenseValidForActionType(this.get(id), options).isValid === true ); } /** * Returns true if action type is enabled or it is a preconfigured action type. */ - public isActionExecutable(actionId: string, actionTypeId: string) { + public isActionExecutable( + actionId: string, + actionTypeId: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { + const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options); return ( - this.isActionTypeEnabled(actionTypeId) || - (!this.isActionTypeEnabled(actionTypeId) && + actionTypeEnabled || + (!actionTypeEnabled && this.preconfiguredActions.find( (preconfiguredAction) => preconfiguredAction.id === actionId ) !== undefined) @@ -118,6 +137,13 @@ export class ActionTypeRegistry { createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create(context), }, }); + // No need to notify usage on basic action types + if (actionType.minimumLicenseRequired !== 'basic') { + this.licensing.featureUsage.register( + getActionTypeFeatureUsageName(actionType as ActionType), + actionType.minimumLicenseRequired + ); + } } /** diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 48122a5ce4e0f..0c16c88ad7a89 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -20,6 +20,7 @@ const createActionsClientMock = () => { execute: jest.fn(), enqueueExecution: jest.fn(), listTypes: jest.fn(), + isActionTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index adef12454f2d5..2b6aec42e0d21 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -8,12 +8,13 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionsClient } from './actions_client'; -import { ExecutorType } from './types'; +import { ExecutorType, ActionType } from './types'; import { ActionExecutor, TaskRunnerFactory, ILicenseState } from './lib'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { actionsConfigMock } from './actions_config.mock'; import { getActionsConfigurationUtilities } from './actions_config'; import { licenseStateMock } from './lib/license_state.mock'; +import { licensingMock } from '../../licensing/server/mocks'; import { elasticsearchServiceMock, @@ -47,6 +48,7 @@ beforeEach(() => { jest.resetAllMocks(); mockedLicenseState = licenseStateMock.create(); actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -299,6 +301,7 @@ describe('create()', () => { }); const localActionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -1244,3 +1247,31 @@ describe('enqueueExecution()', () => { expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts); }); }); + +describe('isActionTypeEnabled()', () => { + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + beforeEach(() => { + actionTypeRegistry.register(fooActionType); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionsClient.isActionTypeEnabled('foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionsClient.isActionTypeEnabled('foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 4079a6ddeeb8a..e565d420d772e 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -343,6 +343,13 @@ export class ActionsClient { public async listTypes(): Promise { return this.actionTypeRegistry.list(); } + + public isActionTypeEnabled( + actionTypeId: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { + return this.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options); + } } function actionFromSavedObject(savedObject: SavedObject): ActionResult { diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index acab6dd41b4b3..f7882849708e5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../actions_config.mock'; import { licenseStateMock } from '../lib/license_state.mock'; +import { licensingMock } from '../../../licensing/server/mocks'; const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook']; @@ -21,6 +22,7 @@ export function createActionTypeRegistry(): { } { const logger = loggingSystemMock.create().get() as jest.Mocked; const actionTypeRegistry = new ActionTypeRegistry({ + licensing: licensingMock.createSetup(), taskManager: taskManagerMock.setup(), taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 7682f01ed769d..33e78ee444cd0 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -23,9 +23,10 @@ beforeEach(() => jest.resetAllMocks()); describe('execute()', () => { test('schedules the action with all given parameters', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), + actionTypeRegistry, isESOUsingEphemeralEncryptionKey: false, preconfiguredActions: [], }); @@ -76,6 +77,9 @@ describe('execute()', () => { }, {} ); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { + notifyUsage: true, + }); }); test('schedules the action with all given parameters with a preconfigured action', async () => { diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index b226583fade52..f0a22c642cf61 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -51,7 +51,7 @@ export function createExecutionEnqueuerFunction({ id ); - if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 692d14e859b34..4ff56536e3867 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -90,6 +90,9 @@ test('successfully executes', async () => { ); expect(actionTypeRegistry.get).toHaveBeenCalledWith('test'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('1', 'test', { + notifyUsage: true, + }); expect(actionType.executor).toHaveBeenCalledWith({ actionId: '1', diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0d4d6de3be1f9..0015b417d72ce 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -102,7 +102,7 @@ export class ActionExecutor { namespace.namespace ); - if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) { + if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } const actionType = actionTypeRegistry.get(actionTypeId); diff --git a/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts b/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts new file mode 100644 index 0000000000000..75919442b2ce6 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionType } from '../types'; + +export function getActionTypeFeatureUsageName(actionType: ActionType) { + return `Connector: ${actionType.name}`; +} diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index e97875b91cf33..4c8e7ab17db68 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -10,6 +10,7 @@ export { TaskRunnerFactory } from './task_runner_factory'; export { ActionExecutor, ActionExecutorContract } from './action_executor'; export { ILicenseState, LicenseState } from './license_state'; export { verifyApiAccess } from './verify_api_access'; +export { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; export { ActionTypeDisabledError, ActionTypeDisabledReason, diff --git a/x-pack/plugins/actions/server/lib/license_state.mock.ts b/x-pack/plugins/actions/server/lib/license_state.mock.ts index d59e9dbdc540f..e5bd9fc9d16cd 100644 --- a/x-pack/plugins/actions/server/lib/license_state.mock.ts +++ b/x-pack/plugins/actions/server/lib/license_state.mock.ts @@ -12,6 +12,7 @@ export const createLicenseStateMock = () => { getLicenseInformation: jest.fn(), ensureLicenseForActionType: jest.fn(), isLicenseValidForActionType: jest.fn(), + setNotifyUsage: jest.fn(), checkLicense: jest.fn().mockResolvedValue({ state: 'valid', }), diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index 32c3c54faf007..06148b1825e73 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -55,6 +55,7 @@ describe('checkLicense()', () => { describe('isLicenseValidForActionType', () => { let license: Subject; let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -67,6 +68,7 @@ describe('isLicenseValidForActionType', () => { beforeEach(() => { license = new Subject(); licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); }); test('should return false when license not defined', () => { @@ -113,11 +115,42 @@ describe('isLicenseValidForActionType', () => { isValid: true, }); }); + + test('should not call notifyUsage by default', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.isLicenseValidForActionType(fooActionType); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should not call notifyUsage on basic action types', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + licenseState.isLicenseValidForActionType({ + ...fooActionType, + minimumLicenseRequired: 'basic', + }); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should call notifyUsage when specified', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.isLicenseValidForActionType(fooActionType, { notifyUsage: true }); + expect(mockNotifyUsage).toHaveBeenCalledWith('Connector: Foo'); + }); }); describe('ensureLicenseForActionType()', () => { let license: Subject; let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -130,6 +163,7 @@ describe('ensureLicenseForActionType()', () => { beforeEach(() => { license = new Subject(); licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); }); test('should throw when license not defined', () => { @@ -178,6 +212,15 @@ describe('ensureLicenseForActionType()', () => { license.next(goldLicense); licenseState.ensureLicenseForActionType(fooActionType); }); + + test('should call notifyUsage', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.ensureLicenseForActionType(fooActionType); + expect(mockNotifyUsage).toHaveBeenCalledWith('Connector: Foo'); + }); }); function createUnavailableLicense() { diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 1686d0201e96c..902fadb3da170 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -11,6 +11,8 @@ import { ILicense } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { ActionType } from '../types'; import { ActionTypeDisabledError } from './errors'; +import { LicensingPluginStart } from '../../../licensing/server'; +import { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; export type ILicenseState = PublicMethodsOf; @@ -24,6 +26,7 @@ export class LicenseState { private licenseInformation: ActionsLicenseInformation = this.checkLicense(undefined); private subscription: Subscription; private license?: ILicense; + private _notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage'] | null = null; constructor(license$: Observable) { this.subscription = license$.subscribe(this.updateInformation.bind(this)); @@ -34,6 +37,10 @@ export class LicenseState { this.licenseInformation = this.checkLicense(license); } + public setNotifyUsage(notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage']) { + this._notifyUsage = notifyUsage; + } + public clean() { this.subscription.unsubscribe(); } @@ -43,8 +50,13 @@ export class LicenseState { } public isLicenseValidForActionType( - actionType: ActionType + actionType: ActionType, + { notifyUsage }: { notifyUsage: boolean } = { notifyUsage: false } ): { isValid: true } | { isValid: false; reason: 'unavailable' | 'expired' | 'invalid' } { + if (notifyUsage) { + this.notifyUsage(actionType); + } + if (!this.license?.isAvailable) { return { isValid: false, reason: 'unavailable' }; } @@ -65,7 +77,16 @@ export class LicenseState { } } + private notifyUsage(actionType: ActionType) { + // No need to notify usage on basic action types + if (this._notifyUsage && actionType.minimumLicenseRequired !== 'basic') { + this._notifyUsage(getActionTypeFeatureUsageName(actionType)); + } + } + public ensureLicenseForActionType(actionType: ActionType) { + this.notifyUsage(actionType); + const check = this.isLicenseValidForActionType(actionType); if (check.isValid) { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 9d545600e61ee..7f7f9e196da07 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -211,6 +211,7 @@ describe('Actions Plugin', () => { features: featuresPluginMock.createSetup(), }; pluginsStart = { + licensing: licensingMock.createStart(), taskManager: taskManagerMock.createStart(), encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), }; @@ -255,5 +256,49 @@ describe('Actions Plugin', () => { ); }); }); + + describe('isActionTypeEnabled()', () => { + const actionType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + + it('passes through the notifyUsage option when set to true', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType(actionType); + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginStart.isActionTypeEnabled('my-action-type', { notifyUsage: true }); + expect(pluginsStart.licensing.featureUsage.notifyUsage).toHaveBeenCalledWith( + 'Connector: My action type' + ); + }); + }); + + describe('isActionExecutable()', () => { + const actionType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + + it('passes through the notifyUsage option when set to true', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType(actionType); + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginStart.isActionExecutable('123', 'my-action-type', { notifyUsage: true }); + expect(pluginsStart.licensing.featureUsage.notifyUsage).toHaveBeenCalledWith( + 'Connector: My action type' + ); + }); + }); }); }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1a15a5a815195..ef20ffbb9ee68 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -26,7 +26,7 @@ import { EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -93,8 +93,12 @@ export interface PluginSetupContract { } export interface PluginStartContract { - isActionTypeEnabled(id: string): boolean; - isActionExecutable(actionId: string, actionTypeId: string): boolean; + isActionTypeEnabled(id: string, options?: { notifyUsage: boolean }): boolean; + isActionExecutable( + actionId: string, + actionTypeId: string, + options?: { notifyUsage: boolean } + ): boolean; getActionsClientWithRequest(request: KibanaRequest): Promise>; getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; preconfiguredActions: PreConfiguredAction[]; @@ -113,6 +117,7 @@ export interface ActionsPluginsSetup { export interface ActionsPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; + licensing: LicensingPluginStart; } const includedHiddenTypes = [ @@ -196,6 +201,7 @@ export class ActionsPlugin implements Plugin, Plugi } const actionTypeRegistry = new ActionTypeRegistry({ + licensing: plugins.licensing, taskRunnerFactory, taskManager: plugins.taskManager, actionsConfigUtils, @@ -268,6 +274,7 @@ export class ActionsPlugin implements Plugin, Plugi public start(core: CoreStart, plugins: ActionsPluginsStart): PluginStartContract { const { logger, + licenseState, actionExecutor, actionTypeRegistry, taskRunnerFactory, @@ -278,6 +285,8 @@ export class ActionsPlugin implements Plugin, Plugi getUnsecuredSavedObjectsClient, } = this; + licenseState?.setNotifyUsage(plugins.licensing.featureUsage.notifyUsage); + const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ includedHiddenTypes, }); @@ -368,11 +377,15 @@ export class ActionsPlugin implements Plugin, Plugi scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); return { - isActionTypeEnabled: (id) => { - return this.actionTypeRegistry!.isActionTypeEnabled(id); + isActionTypeEnabled: (id, options = { notifyUsage: false }) => { + return this.actionTypeRegistry!.isActionTypeEnabled(id, options); }, - isActionExecutable: (actionId: string, actionTypeId: string) => { - return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); + isActionExecutable: ( + actionId: string, + actionTypeId: string, + options = { notifyUsage: false } + ) => { + return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId, options); }, getActionsAuthorizationWithRequest(request: KibanaRequest) { return instantiateAuthorization(request); diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index ef3a9e42b983f..88abce7298622 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -1038,6 +1038,11 @@ export class AlertsClient { const actionsClient = await this.getActionsClient(); const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; const actionResults = await actionsClient.getBulk(actionIds); + const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; + actionTypeIds.forEach((id) => { + // Notify action type usage via "isActionTypeEnabled" function + actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); + }); alertActions.forEach(({ id, ...alertAction }, i) => { const actionResultValue = actionResults.find((action) => action.id === id); if (actionResultValue) { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 65a30d1750149..56e868732e3fb 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -10,9 +10,9 @@ import { taskManagerMock } from '../../../../task_manager/server/task_manager.mo import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; -import { actionsClientMock, actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; -import { ActionsAuthorization } from '../../../../actions/server'; +import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -374,6 +374,10 @@ describe('create()', () => { "scheduledTaskId": "task-123", } `); + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); }); test('creates an alert with multiple actions', async () => { @@ -690,7 +694,11 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); - const actionsClient = actionsClientMock.create(); + // Reset from default behaviour + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + actionsClient.getBulk.mockReset(); actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index 14275575f75f4..60b5b62954f05 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -15,7 +15,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { resolvable } from '../../test_utils'; -import { ActionsAuthorization } from '../../../../actions/server'; +import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -319,6 +319,11 @@ describe('update()', () => { "version": "123", } `); + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true }); }); it('calls the createApiKey function', async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index aca447b6adedd..21e642d228b4d 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -86,7 +86,9 @@ export function createExecutionHandler({ const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; for (const action of actions) { - if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) { + if ( + !actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true }) + ) { logger.warn( `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` ); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index f3542c728845d..2fa9fbe18730c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -54,5 +54,31 @@ export default function createActionTests({ getService }: FtrProviderContext) { id: response.body.id, }); }); + + it('should notify feature usage when creating a gold action type', async () => { + const testStart = new Date(); + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, response.body.id, 'action', 'actions'); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(testStart.getTime()); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index f74c6eaa3298a..2316585d2d0f4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -216,6 +216,40 @@ export default function ({ getService }: FtrProviderContext) { }, }); }); + + it('should notify feature usage when executing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const executionStart = new Date(); + await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(executionStart.getTime()); + }); }); interface ValidateEventLogParams { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index 81db8177b2c11..e06aec72f1874 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -120,5 +121,41 @@ export default function updateActionTests({ getService }: FtrProviderContext) { message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, }); }); + + it('should notify feature usage when editing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const updateStart = new Date(); + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type updated', + secrets: {}, + config: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(updateStart.getTime()); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index b94a547452377..40d88a6bface5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -343,5 +343,30 @@ instanceStateValue: true }, }); }); + + it('should notify feature usage when using a gold action type', async () => { + const testStart = new Date(); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ reference }); + expect(response.statusCode).to.eql(200); + + // Wait for alert to run + await esTestIndexTool.waitForDocs('action:test.index-record', reference); + + const { + body: { features }, + } = await supertestWithoutAuth.get(`${getUrlPrefix(space.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const indexRecordFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Index Record' + ); + expect(indexRecordFeature).to.be.ok(); + expect(indexRecordFeature.last_used).to.be.a('string'); + expect(new Date(indexRecordFeature.last_used).getTime()).to.be.greaterThan( + testStart.getTime() + ); + + await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + }); }); } From 20edc752765efe7ca720d908f778ae08e85db466 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 15 Oct 2020 12:59:28 -0600 Subject: [PATCH 23/79] [data.ui] Fix flaky test & lazy loading rendering artifacts. (#80612) --- .../apply_filters/apply_filters_popover.tsx | 7 +- .../data/public/ui/filter_bar/index.tsx | 7 +- .../public/ui/index_pattern_select/index.tsx | 7 +- .../public/ui/query_string_input/index.tsx | 7 +- .../query_string_input/query_bar_top_row.tsx | 6 +- .../data/public/ui/search_bar/index.tsx | 7 +- .../public/ui/search_bar/search_bar.test.tsx | 104 +++++++----------- .../data/public/ui/search_bar/search_bar.tsx | 2 +- .../public/ui/shard_failure_modal/index.tsx | 7 +- .../data/public/ui/typeahead/index.tsx | 7 +- 10 files changed, 54 insertions(+), 107 deletions(-) diff --git a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx index 80e1a26163b72..19606cafc5c8a 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -18,17 +18,12 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { IIndexPattern, Filter } from '../..'; type CancelFnType = () => void; type SubmitFnType = (filters: Filter[]) => void; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyApplyFiltersPopoverContent = React.lazy(() => import('./apply_filter_popover_content')); diff --git a/src/plugins/data/public/ui/filter_bar/index.tsx b/src/plugins/data/public/ui/filter_bar/index.tsx index b4296bb6615d4..4d9ba69afd48e 100644 --- a/src/plugins/data/public/ui/filter_bar/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { FilterLabelProps } from './filter_editor/lib/filter_label'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); export const FilterLabel = (props: FilterLabelProps) => ( diff --git a/src/plugins/data/public/ui/index_pattern_select/index.tsx b/src/plugins/data/public/ui/index_pattern_select/index.tsx index f0db37eb963fd..c909b202a4094 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { IndexPatternSelectInternalProps } from './index_pattern_select'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyIndexPatternSelect = React.lazy(() => import('./index_pattern_select')); export const IndexPatternSelect = (props: IndexPatternSelectInternalProps) => ( diff --git a/src/plugins/data/public/ui/query_string_input/index.tsx b/src/plugins/data/public/ui/query_string_input/index.tsx index 5bc5bd5097969..eb6641bf3661e 100644 --- a/src/plugins/data/public/ui/query_string_input/index.tsx +++ b/src/plugins/data/public/ui/query_string_input/index.tsx @@ -18,16 +18,11 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { withKibana } from '../../../../kibana_react/public'; import type { QueryBarTopRowProps } from './query_bar_top_row'; import type { QueryStringInputProps } from './query_string_input'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyQueryBarTopRow = React.lazy(() => import('./query_bar_top_row')); export const QueryBarTopRow = (props: QueryBarTopRowProps) => ( diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index e01fbedbe38de..7a44b924870f0 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -36,12 +36,14 @@ import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; -import { useKibana, toMountPoint } from '../../../../kibana_react/public'; -import { QueryStringInput } from './'; +import { useKibana, toMountPoint, withKibana } from '../../../../kibana_react/public'; +import QueryStringInputUI from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; +const QueryStringInput = withKibana(QueryStringInputUI); + // @internal export interface QueryBarTopRowProps { query?: Query; diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index d81ed7333655d..310542f4b12bd 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -19,15 +19,10 @@ import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { withKibana } from '../../../../kibana_react/public'; import type { SearchBarProps } from './search_bar'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazySearchBar = React.lazy(() => import('./search_bar')); const WrappedSearchBar = (props: SearchBarProps) => ( diff --git a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx index a89b9bb7f91ef..74992f35fffc8 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx @@ -18,10 +18,7 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/dom'; -import { render } from '@testing-library/react'; - -import { SearchBar } from './'; +import SearchBar from './search_bar'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -29,6 +26,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); +import { mount } from 'enzyme'; import { IIndexPattern } from '../..'; const mockTimeHistory = { @@ -37,16 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('..', () => { +jest.mock('../filter_bar/filter_bar', () => { return { FilterBar: () =>
, }; }); -jest.mock('../query_string_input', () => { - return { - QueryBarTopRow: () =>
, - }; +jest.mock('../query_string_input/query_bar_top_row', () => { + return () =>
; }); const noop = jest.fn(); @@ -117,48 +113,42 @@ function wrapSearchBarInContext(testProps: any) { ); } -// FLAKY: https://github.com/elastic/kibana/issues/79910 -describe.skip('SearchBar', () => { - const SEARCH_BAR_TEST_ID = 'globalQueryBar'; +describe('SearchBar', () => { const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.globalFilterBar'; + const FILTER_BAR = '.filterBar'; const QUERY_BAR = '.queryBar'; beforeEach(() => { jest.clearAllMocks(); }); - it('Should render query bar when no options provided (in reality - timepicker)', async () => { - const { container, getByTestId } = render( + it('Should render query bar when no options provided (in reality - timepicker)', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(1); }); - it('Should render empty when timepicker is off and no options provided', async () => { - const { container, getByTestId } = render( + it('Should render empty when timepicker is off and no options provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render filter bar, when required fields are provided', async () => { - const { container, getByTestId } = render( + it('Should render filter bar, when required fields are provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, @@ -167,15 +157,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(1); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should NOT render filter bar, if disabled', async () => { - const { container, getByTestId } = render( + it('Should NOT render filter bar, if disabled', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showFilterBar: false, @@ -185,15 +173,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render query bar, when required fields are provided', async () => { - const { container, getByTestId } = render( + it('Should render query bar, when required fields are provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -202,15 +188,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(1); }); - it('Should NOT render query bar, if disabled', async () => { - const { container, getByTestId } = render( + it('Should NOT render query bar, if disabled', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -220,15 +204,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render query bar and filter bar', async () => { - const { container, getByTestId } = render( + it('Should render query bar and filter bar', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -239,10 +221,8 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(1); + expect(component.find(QUERY_BAR).length).toBe(1); }); }); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 95651ac9ed8b3..daa6fa0dd80ab 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -26,7 +26,7 @@ import { get, isEqual } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; -import { QueryBarTopRow } from '../query_string_input'; +import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; import { IDataPluginServices } from '../../types'; import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.tsx b/src/plugins/data/public/ui/shard_failure_modal/index.tsx index cea882deff365..2ac470573c422 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/index.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { ShardFailureOpenModalButtonProps } from './shard_failure_open_modal_button'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyShardFailureOpenModalButton = React.lazy( () => import('./shard_failure_open_modal_button') diff --git a/src/plugins/data/public/ui/typeahead/index.tsx b/src/plugins/data/public/ui/typeahead/index.tsx index aa3c2d71300df..58547cd2ccbec 100644 --- a/src/plugins/data/public/ui/typeahead/index.tsx +++ b/src/plugins/data/public/ui/typeahead/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { SuggestionsComponentProps } from './suggestions_component'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazySuggestionsComponent = React.lazy(() => import('./suggestions_component')); export const SuggestionsComponent = (props: SuggestionsComponentProps) => ( From e1aec1791026e0deb9773536fd4c677cb80ffead Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 15 Oct 2020 13:21:03 -0600 Subject: [PATCH 24/79] [Security Solutions][Detection Engine] Fixes pre-packaged rules which contain exception lists to not overwrite user defined lists (#80592) ## Summary Fixes a bug where when you update your pre-packaged rules you could end up removing any existing exception lists the user might have already added. See: https://github.com/elastic/kibana/issues/80417 * Fixes the merge logic so that any exception lists from pre-packaged rules will be additive if they do not already exist on the rule. User based exception lists will not be lost. * Added new backend integration tests for exception lists that did not exist before including ones that test the functionality of exception lists * Refactored some of the code in the `get_rules_to_update.ts` * Refactored some of the integration tests to use helper utils of `countDownES`, and `countDownTest` which simplify the retry logic within the integration tests * Added unit tests to exercise the bug and then the fix. * Added integration tests that fail this logic and then fixed the logic ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../rules/get_rules_to_update.test.ts | 516 +++++++++++-- .../rules/get_rules_to_update.ts | 64 +- .../tests/create_exceptions.ts | 704 ++++++++++++++++++ .../tests/create_threat_matching.ts | 2 +- .../security_and_spaces/tests/index.ts | 1 + .../detection_engine_api_integration/utils.ts | 218 ++++-- x-pack/test/lists_api_integration/utils.ts | 88 +-- 7 files changed, 1377 insertions(+), 216 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts index 5b2eb24bcdcd2..a21f861c1f348 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts @@ -4,102 +4,470 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRulesToUpdate } from './get_rules_to_update'; +import { filterInstalledRules, getRulesToUpdate, mergeExceptionLists } from './get_rules_to_update'; import { getResult } from '../routes/__mocks__/request_responses'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; describe('get_rules_to_update', () => { - test('should return empty array if both rule sets are empty', () => { - const update = getRulesToUpdate([], []); - expect(update).toEqual([]); - }); + describe('get_rules_to_update', () => { + test('should return empty array if both rule sets are empty', () => { + const update = getRulesToUpdate([], []); + expect(update).toEqual([]); + }); - test('should return empty array if the id of the two rules do not match', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + test('should return empty array if the rule_id of the two rules do not match', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-2'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-2'; + installedRule.params.version = 1; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); - test('should return empty array if the id of file system rule is less than the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 1; + test('should return empty array if the version of file system rule is less than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 2; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 2; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); - test('should return empty array if the id of file system rule is the same as the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 1; + test('should return empty array if the version of file system rule is the same as the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); + + test('should return the rule to update if the version of file system rule is greater than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + installedRule.params.exceptionsList = []; + + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([ruleFromFileSystem]); + }); + + test('should return 1 rule out of 2 to update if the version of file system rule is greater than the installed version of just one', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = []; + + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]); + expect(update).toEqual([ruleFromFileSystem]); + }); + + test('should return 2 rules out of 2 to update if the version of file system rule is greater than the installed version of both', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = []; + + const update = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]); + }); + + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual([ + ...ruleFromFileSystem1.exceptions_list, + ...installedRule1.params.exceptionsList, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - test('should return the rule to update if the id of file system rule is greater than the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([ruleFromFileSystem]); + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list for multiple rules', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.exceptions_list = []; + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update1, update2] = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); + expect(update2.exceptions_list).toEqual(installedRule2.params.exceptionsList); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list for mixed rules', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.exceptions_list = []; + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + ruleFromFileSystem2.exceptions_list = [ + { + id: 'second_list', + list_id: 'second_list', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update1, update2] = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); + expect(update2.exceptions_list).toEqual([ + ...ruleFromFileSystem2.exceptions_list, + ...installedRule2.params.exceptionsList, + ]); + }); }); - test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + describe('filterInstalledRules', () => { + test('should return "false" if the id of the two rules do not match', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-2'; + installedRule.params.version = 1; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); - const installedRule1 = getResult(); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; + test('should return "false" if the version of file system rule is less than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule2 = getResult(); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 2; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]); - expect(update).toEqual([ruleFromFileSystem]); + test('should return "false" if the version of file system rule is the same as the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); + + test('should return "true" to update if the version of file system rule is greater than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + installedRule.params.exceptionsList = []; + + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(true); + }); }); - test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => { - const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem1.rule_id = 'rule-1'; - ruleFromFileSystem1.version = 2; + describe('mergeExceptionLists', () => { + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual([ + ...ruleFromFileSystem1.exceptions_list, + ...installedRule1.params.exceptionsList, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem2.rule_id = 'rule-2'; - ruleFromFileSystem2.version = 2; + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); - const installedRule1 = getResult(); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; - const installedRule2 = getResult(); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - const update = getRulesToUpdate( - [ruleFromFileSystem1, ruleFromFileSystem2], - [installedRule1, installedRule2] - ); - expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]); + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts index 577ad44789bdc..28a58ea49b903 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -7,15 +7,67 @@ import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RuleAlertType } from './types'; +/** + * Returns the rules to update by doing a compare to the rules from the file system against + * the installed rules already. This also merges exception list items between the two since + * exception list items can exist on both rules to update and already installed rules. + * @param rulesFromFileSystem The rules on the file system to check against installed + * @param installedRules The installed rules + */ export const getRulesToUpdate = ( rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], installedRules: RuleAlertType[] ): AddPrepackagedRulesSchemaDecoded[] => { - return rulesFromFileSystem.filter((rule) => - installedRules.some((installedRule) => { - return ( - rule.rule_id === installedRule.params.ruleId && rule.version > installedRule.params.version + return rulesFromFileSystem + .filter((ruleFromFileSystem) => filterInstalledRules(ruleFromFileSystem, installedRules)) + .map((ruleFromFileSystem) => mergeExceptionLists(ruleFromFileSystem, installedRules)); +}; + +/** + * Filters rules from the file system that do not match the installed rules so you only + * get back rules that are going to be updated + * @param ruleFromFileSystem The rules from the file system to check if any are updates + * @param installedRules The installed rules to compare against for updates + */ +export const filterInstalledRules = ( + ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + installedRules: RuleAlertType[] +): boolean => { + return installedRules.some((installedRule) => { + return ( + ruleFromFileSystem.rule_id === installedRule.params.ruleId && + ruleFromFileSystem.version > installedRule.params.version + ); + }); +}; + +/** + * Given a rule from the file system and the set of installed rules this will merge the exception lists + * from the installed rules onto the rules from the file system. + * @param ruleFromFileSystem The rules from the file system that might have exceptions_lists + * @param installedRules The installed rules which might have user driven exceptions_lists + */ +export const mergeExceptionLists = ( + ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + installedRules: RuleAlertType[] +): AddPrepackagedRulesSchemaDecoded => { + if (ruleFromFileSystem.exceptions_list != null) { + const installedRule = installedRules.find( + (ruleToFind) => ruleToFind.params.ruleId === ruleFromFileSystem.rule_id + ); + if (installedRule != null && installedRule.params.exceptionsList != null) { + const installedExceptionList = installedRule.params.exceptionsList; + const fileSystemExceptions = ruleFromFileSystem.exceptions_list.filter((potentialDuplicate) => + installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id) ); - }) - ); + return { + ...ruleFromFileSystem, + exceptions_list: [...fileSystemExceptions, ...installedRule.params.exceptionsList], + }; + } else { + return ruleFromFileSystem; + } + } else { + return ruleFromFileSystem; + } }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts new file mode 100644 index 0000000000000..42d4b86119bb0 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -0,0 +1,704 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; +import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../plugins/lists/common/constants'; + +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_PREPACKAGED_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + waitFor, + getQueryAllSignals, + downgradeImmutableRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('create_rules_with_exceptions', () => { + describe('creating rules with exceptions', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllExceptions(es); + }); + + it('should create a single rule with a rule_id and add an exception list to the rule', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + const expected: Partial = { + ...getSimpleRuleOutput(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(expected); + }); + + it('should create a single rule with an exception list and validate it ran successfully', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + return statusBody[body.id]?.current_status?.status === 'succeeded'; + }); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + const expected: Partial = { + ...getSimpleRuleOutput(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + expect(bodyToCompare).to.eql(expected); + expect(statusBody[body.id].current_status.status).to.eql('succeeded'); + }); + + it('should allow removing an exception list from an immutable rule through patch', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // remove the exceptions list as a user is allowed to remove it from an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list.length).to.eql(0); + }); + + it('should allow adding a second exception list to an immutable rule through patch', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list.length).to.eql(2); + }); + + it('should override any updates to pre-packaged rules if the user removes the exception list through the API but the new version of a rule has an exception list again', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one exception list + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + // remove the exceptions list as a user is allowed to remove it + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the pre-packaged rule after we upgraded it + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // We should have a length of 1 and it should be the same as our original before we tried to remove it using patch + expect(body.exceptions_list.length).to.eql(1); + expect(body.exceptions_list).to.eql(immutableRule.exceptions_list); + }); + + it('should merge back an exceptions_list if it was removed from the immutable rule through PATCH', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // remove the exception list and only have a single list that is not an endpoint_list + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the immutable rule after we installed it a second time + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // The installed rule should have both the original immutable exceptions list back and the + // new list the user added. + expect(body.exceptions_list).to.eql([ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + it('should NOT add an extra exceptions_list that already exists on a rule during an upgrade', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the immutable rule after we installed it a second time + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // The installed rule should have both the original immutable exceptions list back and the + // new list the user added. + expect(body.exceptions_list).to.eql([...immutableRule.exceptions_list]); + }); + + it('should NOT allow updates to pre-packaged rules to overwrite existing exception based rules when the user adds an additional exception list', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // It should be the same as what the user added originally + expect(body.exceptions_list).to.eql([ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + it('should not remove any exceptions added to a pre-packaged/immutable rule during an update if that rule has no existing exception lists', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json + // since this rule does not have existing exceptions_list that we are going to use for tests + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).eql(0); // make sure we have no exceptions_list + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade of the rule to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // ensure that the same exception is still on the rule + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list).to.eql([ + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllExceptions(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute against an exception list that does not include valid entries and get back 10 signals', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMock(), + entries: [ + { + field: 'some.none.existent.field', // non-existent field where we should not exclude anything + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + }; + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(exceptionListItem) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + query: 'host.name: "suricata-sensor-amsterdam"', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait until rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length > 0; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should be able to execute against an exception list that does include valid entries and get back 0 signals', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMock(), + entries: [ + { + field: 'host.name', // This matches the query below which will exclude everything + operator: 'included', + type: 'match', + value: 'suricata-sensor-amsterdam', + }, + ], + }; + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(exceptionListItem) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait for Task Manager to finish executing the rule + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id]?.current_status?.status === 'succeeded'; + }); + + // Get the signals now that we are done running and expect the result to always be zero + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 620e771b3446d..6d3a0ce683cda 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -170,7 +170,7 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(10); }); - it('should be return zero matches if the mapping does not match against anything in the mapping', async () => { + it('should return zero matches if the mapping does not match against anything in the mapping', async () => { const rule: CreateRulesSchema = { ...getCreateThreatMatchRulesSchemaMock(), from: '1900-01-01T00:00:00.000Z', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index cc0eb04075b77..24b76853164f2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -16,6 +16,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); loadTestFile(require.resolve('./create_threat_matching')); + loadTestFile(require.resolve('./create_exceptions')); loadTestFile(require.resolve('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 5d82eed41d3c5..db91529b8a2c3 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Client } from '@elastic/elasticsearch'; +import { ApiResponse, Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { Context } from '@elastic/elasticsearch/lib/Transport'; import { Status, SignalIds, @@ -14,7 +15,10 @@ import { import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; -import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/security_solution/common/constants'; +import { + DETECTION_ENGINE_INDEX_URL, + INTERNAL_RULE_ID_KEY, +} from '../../plugins/security_solution/common/constants'; /** * This will remove server generated properties such as date times, etc... @@ -245,34 +249,38 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial = * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllAlerts = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - const result = await es.deleteByQuery({ - index: '.kibana', - q: 'type:alert', - wait_for_completion: true, - refresh: true, - conflicts: 'proceed', - body: {}, - }); - // deleteByQuery will cause version conflicts as alerts are being updated - // by background processes; the code below accounts for that - if (result.body.version_conflicts !== 0) { - throw new Error(`Version conflicts for ${result.body.version_conflicts} alerts`); - } - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Error in deleteAllAlerts(), retries left: ${retryCount - 1}`, err); +export const deleteAllAlerts = async (es: Client): Promise => { + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:alert', + wait_for_completion: true, + refresh: true, + conflicts: 'proceed', + body: {}, + }); + }, 'deleteAllAlerts'); +}; - // retry, counting down, and delay a bit before - await new Promise((resolve) => setTimeout(resolve, 250)); - await deleteAllAlerts(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllAlerts, no retries are left'); - } +export const downgradeImmutableRule = async (es: Client, ruleId: string): Promise => { + return countDownES(async () => { + return es.updateByQuery({ + index: '.kibana', + refresh: true, + wait_for_completion: true, + body: { + script: { + lang: 'painless', + source: 'ctx._source.alert.params.version--', + }, + query: { + term: { + 'alert.tags': `${INTERNAL_RULE_ID_KEY}:${ruleId}`, + }, + }, + }, + }); + }, 'downgradeImmutableRule'); }; /** @@ -295,27 +303,15 @@ export const deleteAllTimelines = async (es: Client): Promise => { * @param es The ElasticSearch handle */ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - await es.deleteByQuery({ - index: '.kibana', - q: 'type:siem-detection-engine-rule-status', - wait_for_completion: true, - refresh: true, - body: {}, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to deleteAllRulesStatuses, retries left are: ${retryCount - 1}`, - err - ); - await deleteAllRulesStatuses(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllRulesStatuses, no retries are left'); - } + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:siem-detection-engine-rule-status', + wait_for_completion: true, + refresh: true, + body: {}, + }); + }, 'deleteAllRulesStatuses'); }; /** @@ -324,24 +320,12 @@ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promi * @param supertest The supertest client library */ export const createSignalsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to create the signals index, retries left are: ${retryCount - 1}`, - err - ); - await createSignalsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not createSignalsIndex, no retries are left'); - } + await countDownTest(async () => { + await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); + return true; + }, 'createSignalsIndex'); }; /** @@ -349,21 +333,12 @@ export const createSignalsIndex = async ( * @param supertest The supertest client library */ export const deleteSignalsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Failure trying to deleteSignalsIndex, retries left are: ${retryCount - 1}`, err); - await deleteSignalsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteSignalsIndex, no retries are left'); - } + await countDownTest(async () => { + await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); + return true; + }, 'deleteSignalsIndex'); }; /** @@ -616,7 +591,7 @@ export const waitFor = async ( functionToTest: () => Promise, maxTimeout: number = 5000, timeoutWait: number = 10 -) => { +): Promise => { await new Promise(async (resolve, reject) => { let found = false; let numberOfTries = 0; @@ -636,3 +611,82 @@ export const waitFor = async ( } }); }; + +/** + * Does a plain countdown and checks against es queries for either conflicts in the error + * or for any over the wire issues such as timeouts or temp 404's to make the tests more + * reliant. + * @param esFunction The function to test against + * @param esFunctionName The name of the function to print if we encounter errors + * @param retryCount The number of times to retry before giving up (has default) + * @param timeoutWait Time to wait before trying again (has default) + */ +export const countDownES = async ( + esFunction: () => Promise, Context>>, + esFunctionName: string, + retryCount: number = 20, + timeoutWait = 250 +): Promise => { + await countDownTest( + async () => { + const result = await esFunction(); + if (result.body.version_conflicts !== 0) { + // eslint-disable-next-line no-console + console.log(`Version conflicts for ${result.body.version_conflicts}`); + return false; + } else { + return true; + } + }, + esFunctionName, + retryCount, + timeoutWait + ); +}; + +/** + * Does a plain countdown and checks against a boolean to determine if to wait and try again. + * This is useful for over the wire things that can cause issues such as conflict or timeouts + * for testing resiliency. + * @param functionToTest The function to test against + * @param name The name of the function to print if we encounter errors + * @param retryCount The number of times to retry before giving up (has default) + * @param timeoutWait Time to wait before trying again (has default) + */ +export const countDownTest = async ( + functionToTest: () => Promise, + name: string, + retryCount: number = 20, + timeoutWait = 250, + ignoreThrow: boolean = false +) => { + if (retryCount > 0) { + try { + const passed = await functionToTest(); + if (!passed) { + // eslint-disable-next-line no-console + console.log(`Failure trying to ${name}, retries left are: ${retryCount - 1}`); + // retry, counting down, and delay a bit before + await new Promise((resolve) => setTimeout(resolve, timeoutWait)); + await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow); + } + } catch (err) { + if (ignoreThrow) { + throw err; + } else { + // eslint-disable-next-line no-console + console.log( + `Failure trying to ${name}, with exception message of:`, + err.message, + `retries left are: ${retryCount - 1}` + ); + // retry, counting down, and delay a bit before + await new Promise((resolve) => setTimeout(resolve, timeoutWait)); + await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow); + } + } + } else { + // eslint-disable-next-line no-console + console.log(`Could not ${name}, no retries are left`); + } +}; diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 54a13fc027c99..5870239b73ed1 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -15,6 +15,7 @@ import { } from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; import { LIST_INDEX } from '../../plugins/lists/common/constants'; +import { countDownES, countDownTest } from '../detection_engine_api_integration/utils'; /** * Creates the lists and lists items index for use inside of beforeEach blocks of tests @@ -22,24 +23,12 @@ import { LIST_INDEX } from '../../plugins/lists/common/constants'; * @param supertest The supertest client library */ export const createListsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to create the lists index, retries left are: ${retryCount - 1}`, - err - ); - await createListsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not createListsIndex, no retries are left'); - } + return countDownTest(async () => { + await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'createListsIndex'); }; /** @@ -47,21 +36,26 @@ export const createListsIndex = async ( * @param supertest The supertest client library */ export const deleteListsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Failure trying to deleteListsIndex, retries left are: ${retryCount - 1}`, err); - await deleteListsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteListsIndex, no retries are left'); - } + return countDownTest(async () => { + await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'deleteListsIndex'); +}; + +/** + * Creates the exception lists and lists items index for use inside of beforeEach blocks of tests + * This will retry 20 times before giving up and hopefully still not interfere with other tests + * @param supertest The supertest client library + */ +export const createExceptionListsIndex = async ( + supertest: SuperTest +): Promise => { + return countDownTest(async () => { + await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'createListsIndex'); }; /** @@ -159,26 +153,14 @@ export const binaryToString = (res: any, callback: any): void => { * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllExceptions = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - await es.deleteByQuery({ - index: '.kibana', - q: 'type:exception-list or type:exception-list-agnostic', - wait_for_completion: true, - refresh: true, - body: {}, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to deleteAllExceptions, retries left are: ${retryCount - 1}`, - err - ); - await deleteAllExceptions(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllExceptions, no retries are left'); - } +export const deleteAllExceptions = async (es: Client): Promise => { + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:exception-list or type:exception-list-agnostic', + wait_for_completion: true, + refresh: true, + body: {}, + }); + }, 'deleteAllExceptions'); }; From 8f3ec3a73d8ff2d4ad5b3cd3d37c5f91133424fe Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Thu, 15 Oct 2020 15:22:24 -0400 Subject: [PATCH 25/79] Adjusts observability alerting perms to require "all" (#79896) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/server/feature.ts | 2 +- x-pack/plugins/infra/server/features.ts | 4 ++-- x-pack/plugins/uptime/server/kibana.index.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 75d8842d4843b..d597b65040ce6 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -55,7 +55,7 @@ export const APM_FEATURE = { read: [], }, alerting: { - all: Object.values(AlertType), + read: Object.values(AlertType), }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 444530c4d79f0..3767144a1b798 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -51,7 +51,7 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, management: { insightsAndAlerting: ['triggersActions'], @@ -92,7 +92,7 @@ export const LOGS_FEATURE = { catalogue: ['infralogging', 'logs'], api: ['infra'], alerting: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], }, savedObject: { all: [], diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index cd2dc5018e110..730bb2277227e 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -67,7 +67,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor read: [umDynamicSettings.name], }, alerting: { - all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], }, management: { insightsAndAlerting: ['triggersActions'], From aec1eb04dfde2bfb284bde00cef5aa30dcd8efc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Thu, 15 Oct 2020 21:49:19 +0200 Subject: [PATCH 26/79] [Security Solution] Fix the Field dropdown in Timeline data providers resets when scrolled (#80718) --- .../components/edit_data_provider/index.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx index 72386a2b287f1..2bc202c65f6ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx @@ -15,7 +15,6 @@ import { EuiFormRow, EuiPanel, EuiSpacer, - EuiToolTip, } from '@elastic/eui'; import React, { useEffect, useMemo, useState, useCallback } from 'react'; import styled from 'styled-components'; @@ -193,18 +192,16 @@ export const StatefulEditDataProvider = React.memo( - 0 ? updatedField[0].label : null}> - - + From 2b380b658309e0022461a92c7bd4c63ba93cf1e4 Mon Sep 17 00:00:00 2001 From: Charlie Pichette <56399229+charlie-pichette@users.noreply.github.com> Date: Thu, 15 Oct 2020 14:49:53 -0600 Subject: [PATCH 27/79] update template to use the new team label (#80748) --- .github/ISSUE_TEMPLATE/security_solution_bug_report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md index 0c24eb2f973f5..a7a12a6eb379d 100644 --- a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -1,8 +1,8 @@ --- -name: Security Solution Bug Report +name: Bug report for Security Solution about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! title: '[Security Solution]' -labels: Team:Security Solution +labels: 'Team: Security Solution' --- **Describe the bug:** From ff32bb1716dba3bf1cf84169250d12b81b8fe2bf Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 15 Oct 2020 14:24:29 -0700 Subject: [PATCH 28/79] [kbn/optimizer] tweak split chunks options (#80444) Co-authored-by: spalger --- packages/kbn-optimizer/src/worker/webpack.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9678dd5de868b..f632b07dc6c7c 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -64,6 +64,14 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: optimization: { noEmitOnErrors: true, + splitChunks: { + maxAsyncRequests: 10, + cacheGroups: { + default: { + reuseExistingChunk: false, + }, + }, + }, }, externals: [UiSharedDeps.externals], From 10271c50eeea164c5ed3244fae63d0f1c470720d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 15 Oct 2020 15:50:31 -0600 Subject: [PATCH 29/79] [Security Solution] [Maps] Kibana index pattern, comma bug fix (#80208) --- .../components/embeddables/__mocks__/mock.ts | 27 ++++++++++++ .../embeddables/embedded_map_helpers.test.tsx | 44 ++++++++++++++++++- .../embeddables/embedded_map_helpers.tsx | 10 ++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts index 6f8c3e1123854..d2f482c320002 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts @@ -484,3 +484,30 @@ export const mockCCSGlobIndexPattern: IndexPatternSavedObject = { title: '*:*', }, }; + +export const mockCommaFilebeatAuditbeatGlobIndexPattern: IndexPatternSavedObject = { + id: 'filebeat-*,auditbeat-*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'filebeat-*,auditbeat-*', + }, +}; + +export const mockCommaFilebeatAuditbeatCCSGlobIndexPattern: IndexPatternSavedObject = { + id: '*:filebeat-*,*:auditbeat-*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: '*:filebeat-*,*:auditbeat-*', + }, +}; + +export const mockCommaFilebeatExclusionGlobIndexPattern: IndexPatternSavedObject = { + id: 'filebeat-*,-filebeat-7.6.0*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'filebeat-*,-filebeat-7.6.0*', + }, +}; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx index 0c6b90ec2b9dd..c503690e776af 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -12,9 +12,12 @@ import { mockAPMRegexIndexPattern, mockAPMTransactionIndexPattern, mockAuditbeatIndexPattern, + mockCCSGlobIndexPattern, + mockCommaFilebeatAuditbeatCCSGlobIndexPattern, + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockCommaFilebeatExclusionGlobIndexPattern, mockFilebeatIndexPattern, mockGlobIndexPattern, - mockCCSGlobIndexPattern, } from './__mocks__/mock'; const mockEmbeddable = embeddablePluginMock.createStartContract(); @@ -122,5 +125,44 @@ describe('embedded_map_helpers', () => { }); expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]); }); + + test('matches on comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockAuditbeatIndexPattern, + ]); + }); + + test('matches on excluded comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatExclusionGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockCommaFilebeatExclusionGlobIndexPattern, + mockAuditbeatIndexPattern, + ]); + }); + + test('matches on CCS comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatAuditbeatCCSGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices: ['cluster2:filebeat-*', 'cluster1:auditbeat-*'], + }); + expect(matchingIndexPatterns).toEqual([mockCommaFilebeatAuditbeatCCSGlobIndexPattern]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index 25928197590ea..4ac759ea534ec 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -150,7 +150,15 @@ export const findMatchingIndexPatterns = ({ const pattern = kip.attributes.title; return ( !ignoredIndexPatterns.includes(pattern) && - siemDefaultIndices.some((sdi) => minimatch(sdi, pattern)) + siemDefaultIndices.some((sdi) => { + const splitPattern = pattern.split(',') ?? []; + return splitPattern.length > 1 + ? splitPattern.some((p) => { + const isMatch = minimatch(sdi, p); + return isMatch && p.charAt(0) === '-' ? false : isMatch; + }) + : minimatch(sdi, pattern); + }) ); }); } catch { From 29491ac3e69c6705aeaafd5a25c975b838ed8351 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 15 Oct 2020 17:21:33 -0500 Subject: [PATCH 30/79] Fix anomaly alert selection text (#80746) Was using the incorrect value. Add a case to leave out "and above" when on "critical." Add a test and rename files to snake case. Fixes #80441. --- .../index.tsx | 2 +- .../select_anomaly_severity.test.tsx | 43 +++++++++++++++++++ ...verity.tsx => select_anomaly_severity.tsx} | 9 ++-- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx rename x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/{SelectAnomalySeverity.tsx => select_anomaly_severity.tsx} (86%) diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index f910f34d258fd..4f87e13104371 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -17,7 +17,7 @@ import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { AnomalySeverity, SelectAnomalySeverity, -} from './SelectAnomalySeverity'; +} from './select_anomaly_severity'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { EnvironmentField, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx new file mode 100644 index 0000000000000..0db8fa6c9d0d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { IntlProvider } from 'react-intl'; +import { ANOMALY_SEVERITY } from '../../../../../ml/common'; +import { SelectAnomalySeverity } from './select_anomaly_severity'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +describe('SelectAnomalySeverity', () => { + it('shows the correct text for each item', async () => { + const result = render( + {}} + value={ANOMALY_SEVERITY.CRITICAL} + />, + { wrapper: Wrapper } + ); + const button = (await result.findAllByText('critical'))[1]; + + button.click(); + + const options = await result.findAllByTestId( + 'SelectAnomalySeverity option text' + ); + + expect( + options.map((option) => (option.firstChild as HTMLElement)?.innerHTML) + ).toEqual([ + 'score critical ', // Trailing space is intentional here, to keep the i18n simple + 'score major and above', + 'score minor and above', + 'score warning and above', + ]); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx index 468d08339431c..b0513c3b59579 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx @@ -45,11 +45,14 @@ export function SelectAnomalySeverity({ onChange, value }: Props) { -

+

From 5f3e2c05e826c77e7ffa30e03fbfecc2fe974d7e Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 15 Oct 2020 17:25:34 -0500 Subject: [PATCH 31/79] Add Storybook a11y addon (#80069) --- package.json | 9 + .../src/worker/webpack.config.ts | 1 - packages/kbn-storybook/lib/default_config.ts | 7 +- packages/kbn-storybook/package.json | 34 +- packages/kbn-storybook/yarn.lock | 1 - x-pack/package.json | 6 - x-pack/plugins/canvas/scripts/storybook.js | 2 + .../canvas/storybook/storyshots.test.tsx | 1 + yarn.lock | 528 +++++++++--------- 9 files changed, 286 insertions(+), 303 deletions(-) delete mode 120000 packages/kbn-storybook/yarn.lock diff --git a/package.json b/package.json index 951c73dc94021..8a87598aec56d 100644 --- a/package.json +++ b/package.json @@ -253,6 +253,13 @@ "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@percy/agent": "^0.26.0", + "@storybook/addon-a11y": "^6.0.26", + "@storybook/addon-actions": "^6.0.26", + "@storybook/addon-essentials": "^6.0.26", + "@storybook/addon-knobs": "^6.0.26", + "@storybook/addon-storyshots": "^6.0.26", + "@storybook/react": "^6.0.26", + "@storybook/theming": "^6.0.26", "@testing-library/dom": "^7.24.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", @@ -301,6 +308,7 @@ "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", + "@types/loader-utils": "^1.1.3", "@types/lodash": "^4.14.159", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", @@ -346,6 +354,7 @@ "@types/vinyl-fs": "^2.4.11", "@types/webpack": "^4.41.3", "@types/webpack-env": "^1.15.2", + "@types/webpack-merge": "^4.1.5", "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index f632b07dc6c7c..7987dd71f765c 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -23,7 +23,6 @@ import { stringifyRequest } from 'loader-utils'; import webpack from 'webpack'; // @ts-expect-error import TerserPlugin from 'terser-webpack-plugin'; -// @ts-expect-error import webpackMerge from 'webpack-merge'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 1fad9e2a3e087..dc2647b7b5757 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -20,7 +20,12 @@ import { StorybookConfig } from '@storybook/core/types'; export const defaultConfig: StorybookConfig = { - addons: ['@kbn/storybook/preset', '@storybook/addon-knobs', '@storybook/addon-essentials'], + addons: [ + '@kbn/storybook/preset', + '@storybook/addon-a11y', + '@storybook/addon-knobs', + '@storybook/addon-essentials', + ], stories: ['../**/*.stories.tsx'], typescript: { reactDocgen: false, diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 5c57f6893d0c8..cf0bb1262ea73 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -4,37 +4,13 @@ "private": true, "license": "Apache-2.0", "main": "./target/index.js", - "kibana": { - "devOnly": true - }, - "dependencies": { + "devDependencies": { "@kbn/dev-utils": "1.0.0", - "@storybook/addon-actions": "^6.0.16", - "@storybook/addon-essentials": "^6.0.16", - "@storybook/addon-knobs": "^6.0.16", - "@storybook/addon-storyshots": "^6.0.16", - "@storybook/core": "^6.0.16", - "@storybook/react": "^6.0.16", - "@storybook/theming": "^6.0.16", "@types/loader-utils": "^1.1.3", - "@types/webpack": "^4.41.3", - "@types/webpack-env": "^1.15.2", - "@types/webpack-merge": "^4.1.5", - "@kbn/utils": "1.0.0", - "babel-loader": "^8.0.6", - "copy-webpack-plugin": "^6.0.2", - "fast-glob": "2.2.7", - "glob-watcher": "5.0.3", - "jest-specific-snapshot": "2.0.0", - "jest-styled-components": "^7.0.2", - "mkdirp": "0.5.1", - "mini-css-extract-plugin": "0.8.0", - "normalize-path": "^3.0.0", - "react-docgen-typescript-loader": "^3.1.1", - "rxjs": "^6.5.5", - "serve-static": "1.14.1", - "styled-components": "^5.1.0", - "webpack": "^4.41.5" + "@types/webpack-merge": "^4.1.5" + }, + "kibana": { + "devOnly": true }, "scripts": { "build": "tsc", diff --git a/packages/kbn-storybook/yarn.lock b/packages/kbn-storybook/yarn.lock deleted file mode 120000 index 3f82ebc9cdbae..0000000000000 --- a/packages/kbn-storybook/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/x-pack/package.json b/x-pack/package.json index 484a64fdc2628..91fecff094110 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,12 +44,6 @@ "@mapbox/mapbox-gl-draw": "^1.2.0", "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", - "@storybook/addon-actions": "^6.0.16", - "@storybook/addon-essentials": "^6.0.16", - "@storybook/addon-knobs": "^6.0.16", - "@storybook/addon-storyshots": "^6.0.16", - "@storybook/react": "^6.0.16", - "@storybook/theming": "^6.0.16", "@testing-library/dom": "^7.24.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index 23703810569b6..3b06b7e7e2112 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -8,6 +8,8 @@ const path = require('path'); const fs = require('fs'); const del = require('del'); const { run } = require('@kbn/dev-utils'); +// This is included in the main Kibana package.json +// eslint-disable-next-line import/no-extraneous-dependencies const storybook = require('@storybook/react/standalone'); const execa = require('execa'); const { DLL_OUTPUT } = require('./../storybook/constants'); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 1e993f9c54617..420923972ed6a 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -111,6 +111,7 @@ addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), + framework: 'react', test: multiSnapshotWithOptions({}), // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, diff --git a/yarn.lock b/yarn.lock index 200896a9ce1a6..4ed8f513da7da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,17 +2759,39 @@ "@types/node" ">=8.9.0" axios "^0.18.0" -"@storybook/addon-actions@6.0.16", "@storybook/addon-actions@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.16.tgz#869c90291fdfec4a0644e8415f5004cc57e59145" - integrity sha512-kyPGMP2frdhUgJAm6ChqvndaUawwQE9Vx7pN1pk/Q4qnyVlWCneZVojQf0iAgL45q0az0XI1tOPr4ooroaniYg== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-a11y@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-6.0.26.tgz#b71761d9b8f8b340894eb9826d51ce319ce65116" + integrity sha512-sx1Ethl9W3Kfns0qB1v0CoAymKTC+odB+rCjUKM1h/ILS/n8ZzwkzAj0L7DU/6wA0nZwZDQ+1wL2ZN7r+vxr8A== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" + axe-core "^3.5.2" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + react-sizeme "^2.5.2" + regenerator-runtime "^0.13.3" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + +"@storybook/addon-actions@6.0.26", "@storybook/addon-actions@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.26.tgz#d0de9e4d78a8f8f5bf8730c04d0b6d1741c29273" + integrity sha512-9tWbAqSwzWWVz8zwAndZFusZYjIcRYgZUC0LqC8QlH79DgF3ASjw9y97+w1VTTAzdb6LYnsMuSpX6+8m5hrR4g== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -2783,40 +2805,40 @@ util-deprecate "^1.0.2" uuid "^8.0.0" -"@storybook/addon-backgrounds@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.0.16.tgz#cbf909992a86dbbdfea172d3950300e8c2a7de01" - integrity sha512-0sH7hlZh4bHt6zV6QyG3ryNGJsxD42iXVwWdwAShzfWJKGfLy5XwdvHUKkMEBbY9bOPeoI9oMli2RAfsD6juLQ== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-backgrounds@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.0.26.tgz#97cea86cc4fe88b6c0ad8addb2d01712e535aa10" + integrity sha512-Y9t1s4N2PgTxLhO85w+WFnnclZNRdxGpsoiLDPkb63HajZvUa5/ogmIOrfFl5DX2zCpkgNLlskmDcEP6n835cA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" memoizerific "^1.11.3" react "^16.8.3" regenerator-runtime "^0.13.3" -"@storybook/addon-controls@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.0.16.tgz#c7fc765a01cc3a0de397f8b55bfeda3f328e5495" - integrity sha512-RgBOply9o3PYoWI7TNKef2AQixw7l620pT1fCJbXykp/lu17eqKaIa5KYHRE9vEajun5RuEQxGnSzQOV3OZAsA== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/node-logger" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-controls@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.0.26.tgz#4cc4c30ee7bf89ab873158ead4d25d6f7e07ffba" + integrity sha512-K3Oik9ClpShv8Qc6JeNwtmd4yJJcnO2nyaAYYFiyNt+Vsg7zMaDtE2wfvViThNKjX7nUXIeh+OscseIkdWgLuA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/node-logger" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" ts-dedent "^1.1.1" -"@storybook/addon-docs@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.16.tgz#b24983a63c6c9469a418bb1478606626aff42dff" - integrity sha512-7gM/0lQ3mSybpOpQbgR8fjAU+u3zgAWyOM1a+LR7zVn5lNjgBhZD2pfHuwViTeAGG/IIpvmOsd57BKlFJw5TPA== +"@storybook/addon-docs@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.26.tgz#bd7fc1fcdc47bb7992fa8d3254367e8c3bba373d" + integrity sha512-3t8AOPkp8ZW74h7FnzxF3wAeb1wRyYjMmgJZxqzgi/x7K0i1inbCq8MuJnytuTcZ7+EK4HR6Ih7o9tJuAtIBLw== dependencies: "@babel/generator" "^7.9.6" "@babel/parser" "^7.9.6" @@ -2826,18 +2848,18 @@ "@mdx-js/loader" "^1.5.1" "@mdx-js/mdx" "^1.5.1" "@mdx-js/react" "^1.5.1" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.0.16" - "@storybook/postinstall" "6.0.16" - "@storybook/source-loader" "6.0.16" - "@storybook/theming" "6.0.16" + "@storybook/node-logger" "6.0.26" + "@storybook/postinstall" "6.0.26" + "@storybook/source-loader" "6.0.26" + "@storybook/theming" "6.0.26" acorn "^7.1.0" acorn-jsx "^5.1.0" acorn-walk "^7.0.0" @@ -2857,36 +2879,36 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/addon-essentials@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.0.16.tgz#031b05f6a9947fd93a86f28767b1c354e8ea4237" - integrity sha512-tHH2B4cGYihaPytzIlcFlc/jDSu1PUMgaQM4uzIDOn6SCYZJMp5vygK97zF7hf41x/TXv+8i9ZMN5iUJ7l1+fw== - dependencies: - "@storybook/addon-actions" "6.0.16" - "@storybook/addon-backgrounds" "6.0.16" - "@storybook/addon-controls" "6.0.16" - "@storybook/addon-docs" "6.0.16" - "@storybook/addon-toolbars" "6.0.16" - "@storybook/addon-viewport" "6.0.16" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/node-logger" "6.0.16" +"@storybook/addon-essentials@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.0.26.tgz#1962f4fd19a9d9a1d1fad152bbfc3bba90f45216" + integrity sha512-AsKcPrVFksYNWq07jKXX/GRcdTa6Uo3sTEwuV5ZazWltlbOIcI0YdQs6mCFaCElB5Dqg1jqyxZ3vcd+VHiRSkA== + dependencies: + "@storybook/addon-actions" "6.0.26" + "@storybook/addon-backgrounds" "6.0.26" + "@storybook/addon-controls" "6.0.26" + "@storybook/addon-docs" "6.0.26" + "@storybook/addon-toolbars" "6.0.26" + "@storybook/addon-viewport" "6.0.26" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/node-logger" "6.0.26" core-js "^3.0.1" regenerator-runtime "^0.13.3" ts-dedent "^1.1.1" -"@storybook/addon-knobs@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.0.16.tgz#ef7b9a67c5f3f75579af1d3c2c1f36205f77f505" - integrity sha512-//4Fq70M7LLOghM6+eugL53QHVmlbBm5240u+Aq2nWQLUtaszrPW6/7Vj0XRwLyp/DQtEHetTE/fFfCLoGK+dw== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-knobs@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.0.26.tgz#c574a817c8d791aced89a272eb0c6baaee9a0bdf" + integrity sha512-a25hOepctnsqX1nym80HLKrn8fvXFqsbcL3ZkAZWAIXZhf+zPYTJFrw25FxvbyhktmjQv6l89jteAfGfC0g8DA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" copy-to-clipboard "^3.0.8" core-js "^3.0.1" escape-html "^1.0.3" @@ -2900,15 +2922,15 @@ react-select "^3.0.8" regenerator-runtime "^0.13.3" -"@storybook/addon-storyshots@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-6.0.16.tgz#e912273966d4c7cba1a9053d6a76e8856e3b834f" - integrity sha512-wQhM6pnjUCLTr/6BMXTptGeqiMPnnTrvLeaRwG1cDChGK/qs3YqTsa2QqLXQ17IvNUDTHLUNQlYk5af+HrCGhg== +"@storybook/addon-storyshots@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-6.0.26.tgz#529a557b4a8e4558da22a8ce847b88f9fb3ab5fa" + integrity sha512-XLt7aqjp3lH9ye5zfgbcZIDe8B9riu9shOsJfhZ1DpzfXxb8NVgAcvsXyMW/7dJZ/paAadXAeZZtWnOBuqNnmw== dependencies: "@jest/transform" "^26.0.0" - "@storybook/addons" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/core" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/core" "6.0.26" "@types/glob" "^7.1.1" "@types/jest" "^25.1.1" "@types/jest-specific-snapshot" "^0.5.3" @@ -2922,62 +2944,62 @@ regenerator-runtime "^0.13.3" ts-dedent "^1.1.1" -"@storybook/addon-toolbars@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.0.16.tgz#704a5d506b8d952eca6e5dca96c00b22aedf495f" - integrity sha512-6ulvPqe38NJRbQp0zajeNsDJQKZzGqbCMsSw3gtkFOMt8D/V625MF8YY/Y9UZ+xHWor17GUgE1k9hljdyZe1Nw== +"@storybook/addon-toolbars@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.0.26.tgz#650a1793caac6616f4481116f4dfb79f2d3c336b" + integrity sha512-f9OI7ix0lQWg4eEHheWYB3dz7kYO6qCGkzp+oIQkPpjnYmY8ZghyRM+ui6vfq+G8BwxrAKGR0CB8ttNxVsd/9A== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" core-js "^3.0.1" -"@storybook/addon-viewport@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.0.16.tgz#574cc0a3f991ce405ba4a3540132fb05edf488f6" - integrity sha512-3vk6lBZrKJrK9rwxglLT1p579WkLvoJxgW5ddpvSsu31NPAKfDufkDqOZOQGyMmcgIFzZJEc9eKjoTcLiHxppw== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-viewport@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.0.26.tgz#c913dadcb55b31d2df21a580e932b85b1a200a8b" + integrity sha512-LdVW61iZhUf2npNk3qPH9DTunVMhKcyiFq2CRlgxcA5FwjdkAbcPiYMc18rfyTvp/Zd2idartvwYalBYpJhAhw== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" global "^4.3.2" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.3" -"@storybook/addons@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.16.tgz#a20a219bd5b1474ad02b92e79a74652898a684d9" - integrity sha512-jGMaOJYTM2yZeX1tI6whEn+4xpI1aAybZBrc+OD21CcGoQrbF/jplZMq7xKI0Y6vOMguuTGulpUNCezD3LbBjA== - dependencies: - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/router" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addons@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.26.tgz#343cbea3eee2d39413b80bc2d66535a7f61488fc" + integrity sha512-OhAApFKgsj9an7FLYfHI4cJQuZ4Zm6yoGOpaxhOvKQMw7dXUPsLvbCyw/6dZOLvaFhjJjQiXtbxtZG+UjR8nvA== + dependencies: + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" global "^4.3.2" regenerator-runtime "^0.13.3" -"@storybook/api@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.16.tgz#56cdfc6f7a21d62d1a4ab06b4741c1560160d320" - integrity sha512-RTC4BKmH5i8bJUQejOHEtjebVKtOaHkmEagI2HQRalsokBc1GLAf84EGrO2TaZiRrItAPL5zZQgEnKUblsGJGw== +"@storybook/api@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.26.tgz#c45222c132eb8bc2e383536adfebbeb7a89867d0" + integrity sha512-aszDoz1c6T+eRtTUwWvySoyd3gRXmQxsingD084NnEp4VfFLA5H7VS/0sre0ZvU5GWh8d9COxY0DS2Ry/QSKvw== dependencies: "@reach/router" "^1.3.3" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/router" "6.0.16" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/reach__router" "^1.3.5" core-js "^3.0.1" fast-deep-equal "^3.1.1" @@ -2991,38 +3013,38 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.16.tgz#a617578c49543b0de9f53eb28daae2bd3c9e1754" - integrity sha512-66B4FH5R7k9i7LBhGsr/hYOxwE4UBM1JMPGV0rhAnFY8m91GiUWl4YWTRdbYIkeaZxf/0oT4sgPScqz44hnw6Q== +"@storybook/channel-postmessage@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.26.tgz#a98a0132d6bdf06741afac2607e9feabe34ab98b" + integrity sha512-FT6lC8M5JlNBxPT0rYfmF1yl9mBv04nfYs82TZpp1CzpLxf7wxdCBZ8SSRmvWIVBoNwGZPDhIk5+6JWyDEISBg== dependencies: - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" core-js "^3.0.1" global "^4.3.2" qs "^6.6.0" telejson "^5.0.2" -"@storybook/channels@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.16.tgz#94e521b9eae535da80afb23feae593aa69bfe75d" - integrity sha512-TsI4GA7lKD4L2w6IjODMRfnEOkmvEp4eJDgf3MKm7+sMbxwi1y1d6yrW1UQbnmwoNJWk60ArMN2yqDBV+5MNJQ== +"@storybook/channels@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.26.tgz#3e8678b4b40085081257a39b9e85fab13a19943c" + integrity sha512-H0iUorayYqS+zfhbjd+cYRzAdRLGLWUeWFu2Aa+oJ4/zeAQNL+DafWboHc567RQ4Vb5KqE5QZoCFskWUUYqJYA== dependencies: core-js "^3.0.1" ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/client-api@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.16.tgz#4af47caccf92a31326ab77c5094dd4f90f888b91" - integrity sha512-fFsp53lt9W2QHSumqdfFRbh+DI9fvd7li0GDxqLeNESXaUVw48yg8lQiyRNK+j5Pl4VBS3AqytLugJ+0MGm2cA== +"@storybook/client-api@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.26.tgz#ac9334ba86834e5cb23fc4fb577de60bda66164d" + integrity sha512-Qd5wR5b5lio/EchuJMhAmmJAE1pfvnEyu+JnyFGwMZLV9mN9NSspz+YsqbSCCDZsYcP5ewvPEnumIWqmj/wagQ== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/channel-postmessage" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.0" "@types/webpack-env" "^1.15.2" @@ -3036,22 +3058,22 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/client-logger@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.16.tgz#6265d2b869a82be64538eaac39470e3845c9e069" - integrity sha512-xM61Aewxqoo8500UxV7iPpfqwikITojiCX3+w8ZiCJ2NizSaXkis95TEFAeHqyozfNym5CqG+6v2NWvGYV3ncQ== +"@storybook/client-logger@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.26.tgz#e3d28bd8dc02ec2c53a9d69773a68189590b746f" + integrity sha512-VNoL6/oehVhn3hZi9vrTNT+C/3oAZKV+smfZFnPtsCR/Fq7CKbmsBd0pGPL57f81RU8e8WygwrIlAGJTDSNIjw== dependencies: core-js "^3.0.1" global "^4.3.2" -"@storybook/components@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.16.tgz#d4c797f7897cefa11bbdb8dfd07bb3d4fa66b3e9" - integrity sha512-zpYGt3tWiN0yT7V0VhBl2T5Mr0COiNnTQUGCpA9Gl3pUBmAov2jCVf1sUxsIcBcMMZmDRcfo6NbJ/LqCFeUg+Q== +"@storybook/components@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.26.tgz#e1f6e16aae850a71c9ac7bdd1d44a068ec9cfdc1" + integrity sha512-8wigI1pDFJO1m1IQWPguOK+nOsaAVRWkVdu+2te/rDcIR9QNvMzzou0+Lhfp3zKSVT4E6mEoGB/TWXXF5Iq0sQ== dependencies: - "@storybook/client-logger" "6.0.16" + "@storybook/client-logger" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/overlayscrollbars" "^1.9.0" "@types/react-color" "^3.0.1" "@types/react-syntax-highlighter" "11.0.4" @@ -3072,17 +3094,17 @@ react-textarea-autosize "^8.1.1" ts-dedent "^1.1.1" -"@storybook/core-events@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.16.tgz#3f8cd525c15fd80c9f327389851cce82a4b96850" - integrity sha512-ib+58N4OY8AOix2qcBH9ICRmVHUocpGaGRVlIo79WxJrpnB/HNQ8pEaniD+OAavDRq1B7uJqFlMkTXCC0GoFiQ== +"@storybook/core-events@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.26.tgz#61181c9a8610d26cc85d47f133a563879044ca2d" + integrity sha512-nWjS/+kMiw31OPgeJQaiFsJk9ZJJo3/d4c+kc6GOl2iC1H3Q4/5cm3NvJBn/7bUtKHmSFwfbDouj+XjUk5rZbQ== dependencies: core-js "^3.0.1" -"@storybook/core@6.0.16", "@storybook/core@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.16.tgz#ec9aa8c0fd1c23d29bf8401b650c0876c41d1b5f" - integrity sha512-dVgw03bB8rSMrYDw+v07Yiqyy4yas1olnXpytscWCWdbBuflSAQU+mtqcHMIH9YlhucIT2dYiErDDDNmqP+6tw== +"@storybook/core@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.26.tgz#ff587929d0f55cefa8405686e831e79aeeb6870e" + integrity sha512-2kmkxbzDJVrjzCjlseffoQJwZRH9bHZUumo5m8gpbN9kVnADER7yd6RUf2Zle5BK3ExC+0PPI1Whfg0qkiXvqw== dependencies: "@babel/plugin-proposal-class-properties" "^7.8.3" "@babel/plugin-proposal-decorators" "^7.8.3" @@ -3105,20 +3127,20 @@ "@babel/preset-react" "^7.8.3" "@babel/preset-typescript" "^7.9.0" "@babel/register" "^7.10.5" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channel-postmessage" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.0.16" - "@storybook/router" "6.0.16" + "@storybook/node-logger" "6.0.26" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" - "@storybook/ui" "6.0.16" + "@storybook/theming" "6.0.26" + "@storybook/ui" "6.0.26" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" "@types/node-fetch" "^2.5.4" @@ -3189,10 +3211,10 @@ dependencies: lodash "^4.17.15" -"@storybook/node-logger@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.16.tgz#805e0748355d13535c3295455f568ea94e57d1ad" - integrity sha512-mD6so/puFV5oByBkDp9rv2mV/WyGy21QdrwXpXdtLDKNgqPuJjHZuF1RA/+MmDK4P1CjvP1no2H5WDKg+aW4QQ== +"@storybook/node-logger@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.26.tgz#2ef95ea1e2defd4efcba6b23431ea5c5cbaa110b" + integrity sha512-mdILu91d/2ZgYfICoAMBjwBAYOgjk2URsPudrs5+23lFoPPIwf4CPWcfgs0f4GdfoICk3kV0W7+8bIARhRKp3g== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.0.0" @@ -3200,23 +3222,23 @@ npmlog "^4.1.2" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.0.16.tgz#77c428534dd10074778dc669f7ffce9f387acc93" - integrity sha512-gZgPNJK/58VepIBodK0pSlD1jPQgIVTEFWot5/iDjxv9cnSl9V+LbIEW5jZp/lzoAONSj8AS646ZZjAM87S4RQ== +"@storybook/postinstall@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.0.26.tgz#3ba9f6fa598d92daf5823361186c4b1369f16ebe" + integrity sha512-B9Dh66MfserWw1J4KbLqfxpnanN//yeDjrrkowzqa3OFLqEPQCekv0ALocovnCkQ13+TcVGjPprxnWXfGhEMpg== dependencies: core-js "^3.0.1" -"@storybook/react@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.16.tgz#21464749f7bd90dc6026235b2ee47acf168d974a" - integrity sha512-cxnBwewx37rL1BjXo3TQFIvvCv9z26r3yuRRWh527/0QODfwGz8TT+/sJHeqBA5JIQzLwAHNqNJhLp6xzfr5Dw== +"@storybook/react@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.26.tgz#5d4b8f2c6d8003912d371298a6e5a945e24680b4" + integrity sha512-X02VpIEhpVc4avYiff861c015++tvMVSXJSrDP5J1xTAglVEiRFcU0Kn5h96o9N8FTup2n2xyj6Y7e8oC9yLXQ== dependencies: "@babel/preset-flow" "^7.0.0" "@babel/preset-react" "^7.0.0" - "@storybook/addons" "6.0.16" - "@storybook/core" "6.0.16" - "@storybook/node-logger" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/core" "6.0.26" + "@storybook/node-logger" "6.0.26" "@storybook/semver" "^7.3.2" "@svgr/webpack" "^5.4.0" "@types/webpack-env" "^1.15.2" @@ -3233,10 +3255,10 @@ ts-dedent "^1.1.1" webpack "^4.43.0" -"@storybook/router@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.16.tgz#b18cc0b1bba477f16f9f2ae8f0eaa0d5ba4b0a0e" - integrity sha512-zijPJ3CR4ytHE0v+pGdaWT3H+es+mLHRkR6hkqcD0ABT5HVfwMlmXJ9FkQGCVpnnNeBOz7+QKCdE13HMelQpqg== +"@storybook/router@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.26.tgz#5b991001afa7d7eb5e40c53cd4c58266b6f9edfd" + integrity sha512-kQ1LF/2gX3IkjS1wX7CsoeBc9ptHQzOsyax16rUyJa769DT5vMNtFtQxjNXMqSiSapPg2yrXJFKQNaoWvKgQEQ== dependencies: "@reach/router" "^1.3.3" "@types/reach__router" "^1.3.5" @@ -3253,31 +3275,31 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.0.16.tgz#a3eb2b0cbede7d9121387738a530d71df645db5d" - integrity sha512-Ub6bU7o2JJUigzu9MSrFH1RD2SmpZZnym+WEidWI9A1gseKp1Rd4KDq36AqJo/oL3hAzoAOirrv3ZixIwXLFMg== +"@storybook/source-loader@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.0.26.tgz#0c9a20b9e018c49d559c56e1bdae8350b8175371" + integrity sha512-axNYEHEj7c9oHUFTMKZ6xRyKZCEEP7Aa9sFPzV5Q3Vrq6/3qhih5fOPXhst6/s4XZC1eIoKKHb/Gk4hmjYOEYA== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/client-logger" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/client-logger" "6.0.26" "@storybook/csf" "0.0.1" core-js "^3.0.1" estraverse "^4.2.0" global "^4.3.2" loader-utils "^2.0.0" lodash "^4.17.15" - prettier "^2.0.5" + prettier "~2.0.5" regenerator-runtime "^0.13.3" -"@storybook/theming@6.0.16", "@storybook/theming@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.16.tgz#dd6de4f29316a6a2380018978b7b4a0ef9ea33c8" - integrity sha512-6D7oMEbeABYZdDY8e3i+O39XLrk6fvG3GBaSGp31BE30d269NcPkGPxMKY/nzc6MY30a+/LbBbM7b6gRKe6b4Q== +"@storybook/theming@6.0.26", "@storybook/theming@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.26.tgz#e5b545fb2653dfd1b043b567197d490b1c3c0da3" + integrity sha512-9yon2ofb9a+RT1pdvn8Njydy7XRw0qXcIsMqGsJRKoZecmRRozqB6DxH9Gbdf1vRSbM9gYUUDjbiMDFz7+4RiQ== dependencies: "@emotion/core" "^10.0.20" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.17" - "@storybook/client-logger" "6.0.16" + "@storybook/client-logger" "6.0.26" core-js "^3.0.1" deep-object-diff "^1.1.0" emotion-theming "^10.0.19" @@ -3287,21 +3309,21 @@ resolve-from "^5.0.0" ts-dedent "^1.1.1" -"@storybook/ui@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.16.tgz#448d2286404554afb13e27fecd9efb0861fa9286" - integrity sha512-4F21kwQVaMwgqoJmO+566j7MXmvPp+7jfWBMPAvyGsf5uIZ4q6V29h5mMLvTOFA4qHw0lHZk2k8V0g5gk/tjCA== +"@storybook/ui@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.26.tgz#60e97d2044a3f63b489d7ad0b0529d93373b71ee" + integrity sha512-Jb7oUJs6uyW+rM4zA8xDn9T0/0XtUAOC/zBl6ofdhYU9rVjYKAQUJqmYgUHNOggq1NGS7BVp1RJIzDWGYEagsA== dependencies: "@emotion/core" "^10.0.20" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/router" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/markdown-to-jsx" "^6.11.0" copy-to-clipboard "^3.0.8" core-js "^3.0.1" @@ -6735,6 +6757,11 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axe-core@^3.5.2: + version "3.5.5" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" + integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== + axe-core@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" @@ -7649,7 +7676,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browser-resolve@^1.11.3, browser-resolve@^1.8.1: +browser-resolve@^1.8.1: version "1.11.3" resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== @@ -11199,7 +11226,7 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -element-resize-detector@^1.1.12: +element-resize-detector@^1.1.12, element-resize-detector@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.1.tgz#b0305194447a4863155e58f13323a0aef30851d1" integrity sha512-BdFsPepnQr9fznNPF9nF4vQ457U/ZJXQDSNF1zBe7yaga8v9AdZf3/NElYxFdUh7SitSGt040QygiTo6dtatIw== @@ -12262,7 +12289,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^24.8.0, expect@^24.9.0: +expect@^24.8.0: version "24.9.0" resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== @@ -12462,7 +12489,7 @@ fast-equals@^2.0.0: resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.0.tgz#bef2c423af3939f2c54310df54c57e64cd2adefc" integrity sha512-u6RBd8cSiLLxAiC04wVsLV6GBFDOXcTCgWkd3wEoFXgidPSoAJENqC9m7Jb2vewSvjBIfXV6icKeh3GTKfIaXA== -fast-glob@2.2.7, fast-glob@^2.0.2, fast-glob@^2.2.6: +fast-glob@^2.0.2, fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== @@ -13672,7 +13699,7 @@ glob-to-regexp@^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== -glob-watcher@5.0.3, glob-watcher@^5.0.3: +glob-watcher@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626" integrity sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg== @@ -17092,7 +17119,7 @@ jest-mock@^26.3.0: "@jest/types" "^26.3.0" "@types/node" "*" -jest-pnp-resolver@^1.2.1, jest-pnp-resolver@^1.2.2: +jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== @@ -17121,17 +17148,6 @@ jest-resolve-dependencies@^26.4.2: jest-regex-util "^26.0.0" jest-snapshot "^26.4.2" -jest-resolve@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" - integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== - dependencies: - "@jest/types" "^24.9.0" - browser-resolve "^1.11.3" - chalk "^2.0.1" - jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" - jest-resolve@^26.4.0, jest-resolve@^26.5.2: version "26.5.2" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.5.2.tgz#0d719144f61944a428657b755a0e5c6af4fc8602" @@ -17212,25 +17228,6 @@ jest-serializer@^26.5.0: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^24.1.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" - integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - expect "^24.9.0" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^24.9.0" - semver "^6.2.0" - jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.4.2.tgz#87d3ac2f2bd87ea8003602fbebd8fcb9e94104f6" @@ -17252,13 +17249,6 @@ jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: pretty-format "^26.4.2" semver "^7.3.2" -jest-specific-snapshot@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-2.0.0.tgz#425fe524b25df154aa39f97fa6fe9726faaac273" - integrity sha512-aXaNqBg/svwEpY5iQEzEHc5I85cUBKgfeVka9KmpznxLnatpjiqjr7QLb/BYNYlsrZjZzgRHTjQJ+Svx+dbdvg== - dependencies: - jest-snapshot "^24.1.0" - jest-specific-snapshot@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-4.0.0.tgz#a52a2e223e7576e610dbeaf341207c557ac20554" @@ -21921,11 +21911,16 @@ prettier@1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== -prettier@^2.0.5, prettier@^2.1.1: +prettier@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== +prettier@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" @@ -23003,6 +22998,16 @@ react-sizeme@^2.3.6: invariant "^2.2.2" lodash "^4.17.4" +react-sizeme@^2.5.2: + version "2.6.12" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.12.tgz#ed207be5476f4a85bf364e92042520499455453e" + integrity sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw== + dependencies: + element-resize-detector "^1.2.1" + invariant "^2.2.4" + shallowequal "^1.1.0" + throttle-debounce "^2.1.0" + react-sizeme@^2.6.7: version "2.6.10" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.10.tgz#9993dcb5e67fab94a8e5d078a0d3820609010f17" @@ -23373,13 +23378,6 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" -realpath-native@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -27845,7 +27843,7 @@ util-extend@^1.0.1: resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8= -util.promisify@1.0.0, util.promisify@^1.0.0, util.promisify@~1.0.0: +util.promisify@1.0.0, util.promisify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== From 2fa083763b4a5092623ba7f511deec608a7715c8 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 15 Oct 2020 16:14:23 -0700 Subject: [PATCH 32/79] [Reporting] Config Schema Validation for rules[N].protocol strings (#80766) --- x-pack/plugins/reporting/server/config/schema.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 8276e8b49d348..7a21c5a1f6104 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -45,7 +45,15 @@ const QueueSchema = schema.object({ const RulesSchema = schema.object({ allow: schema.boolean(), host: schema.maybe(schema.string()), - protocol: schema.maybe(schema.string()), + protocol: schema.maybe( + schema.string({ + validate(value) { + if (!/:$/.test(value)) { + return 'must end in colon'; + } + }, + }) + ), }); const CaptureSchema = schema.object({ From da2f2db646a63b5568662ac47e413e91cd10df8d Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 15 Oct 2020 16:57:45 -0700 Subject: [PATCH 33/79] Emit info log when using custom registry URL (#80768) --- .../server/services/epm/registry/registry_url.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index ff9a7871a7db8..efc25cc2efb5d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -28,12 +28,14 @@ const getDefaultRegistryUrl = (): string => { } }; -// Custom registry URL is currently only for internal Elastic development and is unsupported export const getRegistryUrl = (): string => { const customUrl = appContextService.getConfig()?.registryUrl; const isEnterprise = licenseService.isEnterprise(); if (customUrl && isEnterprise) { + appContextService + .getLogger() + .info('Custom registry url is an experimental feature and is unsupported.'); return customUrl; } From 4ff67805237a4e9ea97d7eb02a8bbbc32638cb3d Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 15 Oct 2020 19:43:39 -0500 Subject: [PATCH 34/79] Fix error rate sorting in services list (#80764) The field was incorrectly labeled `errorsPerMinute` instead of `transactionErrorRate` (probably left over from before when we switched to using error rate.) Use `-1` for the fallback sort so "N/A" appears after "0%" Fixes #80473. --- .../app/ServiceOverview/ServiceList/index.tsx | 12 +++++++----- .../ServiceList/service_list.test.tsx | 2 -- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 49319f167703c..0ce07a3c0ad27 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -153,7 +153,7 @@ export const SERVICE_COLUMNS: Array> = [ width: px(unit * 10), }, { - field: 'errorsPerMinute', + field: 'transactionErrorRate', name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { defaultMessage: 'Error rate %', }), @@ -222,13 +222,15 @@ export function ServiceList({ items, noItemsMessage }: Props) { itemsToSort, (item) => { switch (sortField) { + // Use `?? -1` here so `undefined` will appear after/before `0`. + // In the table this will make the "N/A" items always at the + // bottom/top. case 'avgResponseTime': - return item.avgResponseTime?.value ?? 0; + return item.avgResponseTime?.value ?? -1; case 'transactionsPerMinute': - return item.transactionsPerMinute?.value ?? 0; + return item.transactionsPerMinute?.value ?? -1; case 'transactionErrorRate': - return item.transactionErrorRate?.value ?? 0; - + return item.transactionErrorRate?.value ?? -1; default: return item[sortField as keyof typeof item]; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx index daddd0a60fe1f..73777c2221a5b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx @@ -35,8 +35,6 @@ describe('ServiceList', () => { it('renders with data', () => { expect(() => - // Types of property 'avgResponseTime' are incompatible. - // Type 'null' is not assignable to type '{ value: number | null; timeseries: { x: number; y: number | null; }[]; } | undefined'.ts(2322) renderWithTheme( , { wrapper: Wrapper } From 06fa16d2a313d884111f6e7f217ab7207a4cfea5 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 16 Oct 2020 09:42:10 +0200 Subject: [PATCH 35/79] added brace import to vis editor (#80652) --- src/plugins/vis_default_editor/public/default_editor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index ed94e52ee2399..a7251acfdf75d 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -18,6 +18,7 @@ */ import './index.scss'; +import 'brace/mode/json'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EventEmitter } from 'events'; From b582559bbd4d12d7c9afc4acd3e4ba55b40f54ec Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 16 Oct 2020 10:39:12 +0200 Subject: [PATCH 36/79] [ML] Transforms/DF Analytics: Fix data grid column sorting. (#80618) * [ML] Fix column sorting. * [ML] Tweak sorting. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/data_grid/use_data_grid.tsx | 35 +++++++++++++++---- .../exploration_query_bar.tsx | 4 +-- .../outlier_exploration/use_outlier_data.ts | 31 +++++++++------- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index b97ddb2690982..08b2d48a982d6 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; @@ -93,10 +93,8 @@ export const useDataGrid = ( [columns] ); - return { - chartsVisible, - chartsButtonVisible: true, - columnsWithCharts: columns.map((c, index) => { + const columnsWithCharts = useMemo(() => { + const updatedColumns = columns.map((c, index) => { const chartData = columnCharts.find((cd) => cd.id === c.id); return { @@ -110,7 +108,32 @@ export const useDataGrid = ( /> ) : undefined, }; - }), + }); + + // Sort the columns to be in line with the current order of visible columns. + // EuiDataGrid misses a callback for the order of all available columns, so + // this only can retain the order of visible columns. + return updatedColumns.sort((a, b) => { + // This will always move visible columns above invisible ones. + if (visibleColumns.indexOf(a.id) === -1 && visibleColumns.indexOf(b.id) > -1) { + return 1; + } + if (visibleColumns.indexOf(b.id) === -1 && visibleColumns.indexOf(a.id) > -1) { + return -1; + } + if (visibleColumns.indexOf(a.id) === -1 && visibleColumns.indexOf(b.id) === -1) { + return a.id.localeCompare(b.id); + } + + // If both columns are visible sort by their visible sorting order. + return visibleColumns.indexOf(a.id) - visibleColumns.indexOf(b.id); + }); + }, [columns, columnCharts, chartsVisible, JSON.stringify(visibleColumns)]); + + return { + chartsVisible, + chartsButtonVisible: true, + columnsWithCharts, errorMessage, invalidSortingColumnns, noDataMessage, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index 06bcdfd364d66..c837fcbacdd55 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -149,11 +149,11 @@ export const ExplorationQueryBar: FC = ({ placeholder={ searchInput.language === SEARCH_QUERY_LANGUAGE.KUERY ? i18n.translate('xpack.ml.stepDefineForm.queryPlaceholderKql', { - defaultMessage: 'e.g. {example}', + defaultMessage: 'Search for e.g. {example}', values: { example: 'method : "GET" or status : "404"' }, }) : i18n.translate('xpack.ml.stepDefineForm.queryPlaceholderLucene', { - defaultMessage: 'e.g. {example}', + defaultMessage: 'Search for e.g. {example}', values: { example: 'method:GET OR status:404' }, }) } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index eded8e82a7919..88aa06808e8a7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -52,17 +52,21 @@ export const useOutlierData = ( const needsDestIndexFields = indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; - const columns: EuiDataGridColumn[] = []; - - if (jobConfig !== undefined && indexPattern !== undefined) { - const resultsField = jobConfig.dest.results_field; - const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); - columns.push( - ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => - sortExplorationResultsFields(a.id, b.id, jobConfig) - ) - ); - } + const columns = useMemo(() => { + const newColumns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + newColumns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + return newColumns; + }, [jobConfig, indexPattern]); const dataGrid = useDataGrid( columns, @@ -124,7 +128,10 @@ export const useOutlierData = ( }, [ dataGrid.chartsVisible, jobConfig?.dest.index, - JSON.stringify([searchQuery, dataGrid.visibleColumns]), + // Only trigger when search or the visible columns changes. + // We're only interested in the visible columns but not their order, that's + // why we sort for comparison (and copying it via spread to avoid sort in place). + JSON.stringify([searchQuery, [...dataGrid.visibleColumns].sort()]), ]); const colorRange = useColorRange( From fc5ad4d859185e344fb2e5c263f4a172d84d63ff Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 16 Oct 2020 10:54:46 +0200 Subject: [PATCH 37/79] Lazy load reporting (#80492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: ⚡️ load dynamically reporting management section * refactor: 💡 remove JSX from main plugin entry file * perf: ⚡️ lazy-load CSV sharing panel React component * perf: ⚡️ lazy-load screen capture sharing panel React components * feat: 🎸 show spinner while shring panels are loading --- .../public/components/panel_spinner.tsx | 22 ++++++ .../components/reporting_panel_content.tsx | 2 +- .../reporting_panel_content_lazy.tsx | 24 ++++++ .../screen_capture_panel_content.tsx | 2 +- .../screen_capture_panel_content_lazy.tsx | 24 ++++++ .../public/mount_management_section.tsx | 42 +++++++++++ .../public/{plugin.tsx => plugin.ts} | 74 +++++++++---------- .../register_csv_reporting.tsx | 2 +- .../register_pdf_png_reporting.tsx | 2 +- 9 files changed, 152 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/reporting/public/components/panel_spinner.tsx create mode 100644 x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx create mode 100644 x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx create mode 100644 x-pack/plugins/reporting/public/mount_management_section.tsx rename x-pack/plugins/reporting/public/{plugin.tsx => plugin.ts} (78%) diff --git a/x-pack/plugins/reporting/public/components/panel_spinner.tsx b/x-pack/plugins/reporting/public/components/panel_spinner.tsx new file mode 100644 index 0000000000000..841b7063361b9 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/panel_spinner.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +export const PanelSpinner: React.FC = (props) => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 22b97f45db186..18895f9e623eb 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -13,7 +13,7 @@ import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -interface Props { +export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx new file mode 100644 index 0000000000000..45a7d60a60966 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { lazy, Suspense, FC } from 'react'; +import { PanelSpinner } from './panel_spinner'; +import type { Props } from './reporting_panel_content'; + +const LazyComponent = lazy(() => + import('./reporting_panel_content').then(({ ReportingPanelContent }) => ({ + default: ReportingPanelContent, + })) +); + +export const ReportingPanelContent: FC> = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index 4a62ab2b76508..ff81ced43e0b4 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -12,7 +12,7 @@ import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingPanelContent } from './reporting_panel_content'; -interface Props { +export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx new file mode 100644 index 0000000000000..52080e16dd6a3 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { lazy, Suspense, FC } from 'react'; +import { PanelSpinner } from './panel_spinner'; +import type { Props } from './screen_capture_panel_content'; + +const LazyComponent = lazy(() => + import('./screen_capture_panel_content').then(({ ScreenCapturePanelContent }) => ({ + default: ScreenCapturePanelContent, + })) +); + +export const ScreenCapturePanelContent: FC = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/reporting/public/mount_management_section.tsx b/x-pack/plugins/reporting/public/mount_management_section.tsx new file mode 100644 index 0000000000000..ac737e4a318ac --- /dev/null +++ b/x-pack/plugins/reporting/public/mount_management_section.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CoreSetup, CoreStart } from 'src/core/public'; +import { Observable } from 'rxjs'; +import { ReportListing } from './components/report_listing'; +import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; +import { ILicense } from '../../licensing/public'; +import { ClientConfigType } from './plugin'; +import { ReportingAPIClient } from './lib/reporting_api_client'; + +export async function mountManagementSection( + coreSetup: CoreSetup, + coreStart: CoreStart, + license$: Observable, + pollConfig: ClientConfigType['poll'], + apiClient: ReportingAPIClient, + params: ManagementAppMountParams +) { + render( + + + , + params.element + ); + + return () => { + unmountComponentAtNode(params.element); + }; +} diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.ts similarity index 78% rename from x-pack/plugins/reporting/public/plugin.tsx rename to x-pack/plugins/reporting/public/plugin.ts index cc5964f737988..33f4fd4abf72c 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -5,9 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import React from 'react'; -import ReactDOM from 'react-dom'; import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { @@ -17,21 +14,21 @@ import { Plugin, PluginInitializerContext, } from 'src/core/public'; -import { UiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, + HomePublicPluginStart, } from '../../../../src/plugins/home/public'; -import { ManagementSetup } from '../../../../src/plugins/management/public'; -import { SharePluginSetup } from '../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { durationToNumber } from '../common/schema_utils'; import { JobId, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { JobSummarySet } from './'; import { getGeneralErrorToast } from './components'; -import { ReportListing } from './components/report_listing'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -60,7 +57,25 @@ function handleError(notifications: NotificationsSetup, err: Error): Rx.Observab return Rx.of({ completed: [], failed: [] }); } -export class ReportingPublicPlugin implements Plugin { +export interface ReportingPublicPluginSetupDendencies { + home: HomePublicPluginSetup; + management: ManagementSetup; + licensing: LicensingPluginSetup; + uiActions: UiActionsSetup; + share: SharePluginSetup; +} + +export interface ReportingPublicPluginStartDendencies { + home: HomePublicPluginStart; + management: ManagementStart; + licensing: LicensingPluginStart; + uiActions: UiActionsStart; + share: SharePluginStart; +} + +export class ReportingPublicPlugin + implements + Plugin { private config: ClientConfigType; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { @@ -76,19 +91,7 @@ export class ReportingPublicPlugin implements Plugin { public setup( core: CoreSetup, - { - home, - management, - licensing, - uiActions, - share, - }: { - home: HomePublicPluginSetup; - management: ManagementSetup; - licensing: LicensingPluginSetup; - uiActions: UiActionsSetup; - share: SharePluginSetup; - } + { home, management, licensing, uiActions, share }: ReportingPublicPluginSetupDendencies ) { const { http, @@ -119,24 +122,19 @@ export class ReportingPublicPlugin implements Plugin { title: this.title, order: 1, mount: async (params) => { - const [start] = await getStartServices(); params.setBreadcrumbs([{ text: this.breadcrumbText }]); - ReactDOM.render( - - - , - params.element + const [[start], { mountManagementSection }] = await Promise.all([ + getStartServices(), + import('./mount_management_section'), + ]); + return await mountManagementSection( + core, + start, + license$, + this.config.poll, + apiClient, + params ); - - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; }, }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 451d907199c4c..e90d6786b58f2 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -11,7 +11,7 @@ import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { JobParamsCSV, SearchRequest } from '../../server/export_types/csv/types'; -import { ReportingPanelContent } from '../components/reporting_panel_content'; +import { ReportingPanelContent } from '../components/reporting_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 2dab66187bb25..d17d4af3c0102 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -13,7 +13,7 @@ import { LicensingPluginSetup } from '../../../licensing/public'; import { LayoutParams } from '../../common/types'; import { JobParamsPNG } from '../../server/export_types/png/types'; import { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; -import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; +import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; From 9b540f0bc75cd1d483c07a88ad0959cae369acaa Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 16 Oct 2020 11:07:50 +0200 Subject: [PATCH 38/79] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20separator=20?= =?UTF-8?q?for=20different=20context=20menu=20groups=20(#80498)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../panel_edit_with_drilldowns_and_context_actions.tsx | 2 +- .../public/context_menu/build_eui_context_menu_panels.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx b/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx index e9543814ff015..5ef2cb73b5937 100644 --- a/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx +++ b/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx @@ -39,7 +39,7 @@ export const PanelEditWithDrilldownsAndContextActions: React.FC = () => { const customActionGrouping: Action['grouping'] = [ { id: 'actions', - getDisplayName: () => 'Custom actions', + getDisplayName: () => 'API actions', getIconType: () => 'cloudStormy', order: 20, }, diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 1fdddfc272e94..c7efb6dad326d 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -201,8 +201,10 @@ export async function buildContextMenuForActions({ for (const panel of Object.values(panels)) { if (panel._level === 0) { - // TODO: Add separator line here once it is available in EUI. - // See https://github.com/elastic/eui/pull/4018 + panels.mainMenu.items.push({ + isSeparator: true, + key: panel.id + '__separator', + }); if (panel.items.length > 3) { panels.mainMenu.items.push({ name: panel.title || panel.id, From a1831a6d9dd7318631470aa9a065a1a94e9c04eb Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Fri, 16 Oct 2020 12:30:46 +0300 Subject: [PATCH 39/79] [Search] Client side session service (#76889) * Add a session service and use it in discover and dashboard * check unefined * Start session in visualize * Fix tests * docs * OSS error alignemnt * Adjust error messages in xpack * Add getErrorMessage * Use showError in vizualize Add original error to expression exception * Cleanup * ts, doc and i18n fixes * Fix jest tests * Fix functional test * functional test * ts * Update functional tests * Add unit tests to interceptor and timeout error * expose toasts test function * doc * typos * lint * Cleanup * review 1 * Code review * doc * doc fix * visualization type fix * fix jest * Fix xpack functional test * fix xpack test * code review * Add tracking methods to session service * remove chromium * Fix ts and jest tests * jest + docs * ts fix * siem test * Use session service to show a timeout notification per session + more unit tests * ts and docs * Remove session service from search source (not needed) * Code review * ts * Single active session in FE session service * Cleanup * Don't integrate with dashboard \ visualize Add functional tests for session toast plugin * Typescript * ts * Improve functional tests * es * simplify filter test * wait until loadedw * filter test * delete crypto for now * Select the correct index :facepalm: * timerange * Adjust functional test logic * improved test format @dosant * Handle exceptions * Don't close sessions automatically, warn instead * jest * Adjust functional test * Remove unused code * delete export Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ugin-plugins-data-public.isearchoptions.md | 1 + ...ns-data-public.isearchoptions.sessionid.md | 13 + ...plugin-plugins-data-public.isearchsetup.md | 1 + ...lugins-data-public.isearchsetup.session.md | 13 + ...plugin-plugins-data-public.isearchstart.md | 1 + ...lugins-data-public.isearchstart.session.md | 13 + ...lic.searchinterceptor.handlesearcherror.md | 4 +- ...n-plugins-data-public.searchinterceptor.md | 2 +- ...ugins-data-public.searchinterceptordeps.md | 1 + ...ta-public.searchinterceptordeps.session.md | 11 + ...ugin-plugins-data-server.isearchoptions.md | 1 + ...ns-data-server.isearchoptions.sessionid.md | 13 + .../application/dashboard_app_controller.tsx | 4 - src/plugins/data/common/mocks.ts | 20 ++ .../data/common/search/es_search/types.ts | 5 + src/plugins/data/common/search/index.ts | 1 + .../data/common/search/session/index.ts | 20 ++ .../data/common/search/session/mocks.ts | 29 ++ .../data/common/search/session/types.ts | 41 +++ src/plugins/data/public/public.api.md | 12 +- .../data/public/search/expressions/esaggs.ts | 4 +- src/plugins/data/public/search/mocks.ts | 3 + .../public/search/search_interceptor.test.ts | 327 +++++++++++------- .../data/public/search/search_interceptor.ts | 54 +-- .../data/public/search/search_service.ts | 7 + .../public/search/session_service.test.ts | 43 +++ .../data/public/search/session_service.ts | 80 +++++ src/plugins/data/public/search/types.ts | 12 +- src/plugins/data/server/server.api.md | 1 + .../public/application/angular/discover.js | 4 + test/functional/page_objects/settings_page.ts | 11 + test/functional/services/toasts.ts | 6 + .../plugins/session_notifications/kibana.json | 9 + .../session_notifications/package.json | 18 + .../session_notifications/public/index.ts | 23 ++ .../session_notifications/public/plugin.tsx | 66 ++++ .../session_notifications/public/types.ts | 28 ++ .../session_notifications/tsconfig.json | 18 + .../test_suites/data_plugin/index.ts | 4 +- .../test_suites/data_plugin/index_patterns.ts | 4 +- .../test_suites/data_plugin/session.ts | 83 +++++ x-pack/plugins/data_enhanced/public/plugin.ts | 1 + .../public/search/search_interceptor.test.ts | 3 + .../public/search/search_interceptor.ts | 2 +- .../components/alerts_table/actions.test.tsx | 5 +- 45 files changed, 858 insertions(+), 164 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md create mode 100644 src/plugins/data/common/mocks.ts create mode 100644 src/plugins/data/common/search/session/index.ts create mode 100644 src/plugins/data/common/search/session/mocks.ts create mode 100644 src/plugins/data/common/search/session/types.ts create mode 100644 src/plugins/data/public/search/session_service.test.ts create mode 100644 src/plugins/data/public/search/session_service.ts create mode 100644 test/plugin_functional/plugins/session_notifications/kibana.json create mode 100644 test/plugin_functional/plugins/session_notifications/package.json create mode 100644 test/plugin_functional/plugins/session_notifications/public/index.ts create mode 100644 test/plugin_functional/plugins/session_notifications/public/plugin.tsx create mode 100644 test/plugin_functional/plugins/session_notifications/public/types.ts create mode 100644 test/plugin_functional/plugins/session_notifications/tsconfig.json create mode 100644 test/plugin_functional/test_suites/data_plugin/session.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index c9018b0048aa3..76d0914173447 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,5 +15,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md new file mode 100644 index 0000000000000..b1d569e58bf1d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) + +## ISearchOptions.sessionId property + +A session ID, grouping multiple search requests into a single session. + +Signature: + +```typescript +sessionId?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index b68c4d61e4e03..bbf856480aedd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,5 +17,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md new file mode 100644 index 0000000000000..7f39d9714a3a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) + +## ISearchSetup.session property + +session management + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index 5defe4a647614..4a69e94dd6f58 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,5 +19,6 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md new file mode 100644 index 0000000000000..de25cccd6d27a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) + +## ISearchStart.session property + +session management + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md index 02db74b1a9e91..1c8b6eb41a72e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; +protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; ``` ## Parameters @@ -17,7 +17,7 @@ protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal | e | any | | | request | IKibanaSearchRequest | | | timeoutSignal | AbortSignal | | -| appAbortSignal | AbortSignal | | +| options | ISearchOptions | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index a02a6116d7ae0..40c7055e4c059 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -27,7 +27,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | | [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | | -| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | +| [handleSearchError(e, request, timeoutSignal, options)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | | [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md index 63eb67ce48246..3653394d28b92 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md @@ -15,6 +15,7 @@ export interface SearchInterceptorDeps | Property | Type | Description | | --- | --- | --- | | [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreSetup['http'] | | +| [session](./kibana-plugin-plugins-data-public.searchinterceptordeps.session.md) | ISessionService | | | [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) | Promise<[CoreStart, any, unknown]> | | | [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsSetup | | | [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreSetup['uiSettings'] | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md new file mode 100644 index 0000000000000..40d00483317ba --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [session](./kibana-plugin-plugins-data-public.searchinterceptordeps.session.md) + +## SearchInterceptorDeps.session property + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 21ddaef3a0b94..af96e1413ba0c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,5 +15,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md new file mode 100644 index 0000000000000..03043de5193d2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) + +## ISearchOptions.sessionId property + +A session ID, grouping multiple search requests into a single session. + +Signature: + +```typescript +sessionId?: string; +``` diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 2ae7c6550b0cc..fa45e433050ab 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -144,7 +144,6 @@ export class DashboardAppController { notifications, overlays, chrome, - injectedMetadata, fatalErrors, uiSettings, savedObjects, @@ -527,9 +526,6 @@ export class DashboardAppController { filterManager.getFilters() ); - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; const getDashTitle = () => diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/data/common/mocks.ts new file mode 100644 index 0000000000000..dde70b1d07443 --- /dev/null +++ b/src/plugins/data/common/mocks.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getSessionServiceMock } from './search/session/mocks'; diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index b1c3e5cdd3960..4d3bc088749a9 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -31,6 +31,11 @@ export interface ISearchOptions { * Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. */ strategy?: string; + + /** + * A session ID, grouping multiple search requests into a single session. + */ + sessionId?: string; } export type ISearchRequestParams> = { diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 2ee0db384cf06..e650cf10db87c 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,3 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; +export * from './session'; diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts new file mode 100644 index 0000000000000..d8f7b5091eb8f --- /dev/null +++ b/src/plugins/data/common/search/session/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './types'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts new file mode 100644 index 0000000000000..7d5cd75b57534 --- /dev/null +++ b/src/plugins/data/common/search/session/mocks.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISessionService } from './types'; + +export function getSessionServiceMock(): jest.Mocked { + return { + clear: jest.fn(), + start: jest.fn(), + getSessionId: jest.fn(), + getSession$: jest.fn(), + }; +} diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts new file mode 100644 index 0000000000000..80ab74f1aa14d --- /dev/null +++ b/src/plugins/data/common/search/session/types.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; + +export interface ISessionService { + /** + * Returns the active session ID + * @returns The active session ID + */ + getSessionId: () => string | undefined; + /** + * Returns the observable that emits an update every time the session ID changes + * @returns `Observable` + */ + getSession$: () => Observable; + /** + * Starts a new session + */ + start: () => string; + /** + * Clears the active session. + */ + clear: () => void; +} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 2ed3e440040de..d280b6f1faf7d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1389,6 +1389,7 @@ export type ISearchGeneric = void; } @@ -1987,7 +1993,7 @@ export class SearchInterceptor { // (undocumented) protected getTimeoutMode(): TimeoutErrorMode; // (undocumented) - protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; + protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) @@ -1998,8 +2004,8 @@ export class SearchInterceptor { abortSignal?: AbortSignal; timeout?: number; }): { - combinedSignal: AbortSignal; timeoutSignal: AbortSignal; + combinedSignal: AbortSignal; cleanup: () => void; }; // (undocumented) @@ -2013,6 +2019,8 @@ export interface SearchInterceptorDeps { // (undocumented) http: CoreSetup_2['http']; // (undocumented) + session: ISessionService; + // (undocumented) startServices: Promise<[CoreStart, any, unknown]>; // (undocumented) toasts: ToastsSetup; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 1021ef0f91d52..1f72bda44e4ed 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -149,7 +149,9 @@ const handleCourierRequest = async ({ request.stats(getRequestInspectorStats(requestSearchSource)); try { - const response = await requestSearchSource.fetch({ abortSignal }); + const response = await requestSearchSource.fetch({ + abortSignal, + }); request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 8bad4cd269b3f..836ddb618e746 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -20,11 +20,13 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; import { ISearchSetup, ISearchStart } from './types'; +import { getSessionServiceMock } from '../../common/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), __enhance: jest.fn(), + session: getSessionServiceMock(), }; } @@ -33,6 +35,7 @@ function createStartContract(): jest.Mocked { aggs: searchAggsStartMock(), search: jest.fn(), showError: jest.fn(), + session: getSessionServiceMock(), searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index ade15adc1c3a3..e8a728bb9cec3 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -17,12 +17,14 @@ * under the License. */ -import { CoreSetup } from '../../../../core/public'; +import { CoreSetup, CoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; -import { SearchTimeoutError, PainlessError } from './errors'; +import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; +import { searchServiceMock } from './mocks'; +import { ISearchStart } from '.'; let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; @@ -31,13 +33,61 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); jest.useFakeTimers(); describe('SearchInterceptor', () => { + let searchMock: jest.Mocked; + let mockCoreStart: MockedKeys; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + searchMock = searchServiceMock.createStartContract(); searchInterceptor = new SearchInterceptor({ toasts: mockCoreSetup.notifications.toasts, - startServices: mockCoreSetup.getStartServices(), + startServices: new Promise((resolve) => { + resolve([mockCoreStart, {}, {}]); + }), uiSettings: mockCoreSetup.uiSettings, http: mockCoreSetup.http, + session: searchMock.session, + }); + }); + + describe('showError', () => { + test('Ignores an AbortError', async () => { + searchInterceptor.showError(new AbortError()); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Ignores a SearchTimeoutError', async () => { + searchInterceptor.showError(new SearchTimeoutError(new Error(), TimeoutErrorMode.UPGRADE)); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Renders a PainlessError', async () => { + searchInterceptor.showError( + new PainlessError( + { + body: { + attributes: { + error: { + failed_shards: { + reason: 'bananas', + }, + }, + }, + } as any, + }, + {} as any + ) + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Renders a general error', async () => { + searchInterceptor.showError(new Error('Oopsy')); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).toBeCalledTimes(1); }); }); @@ -49,149 +99,172 @@ describe('SearchInterceptor', () => { params: {}, }; const response = searchInterceptor.search(mockRequest); - - const result = await response.toPromise(); - expect(result).toBe(mockResponse); + expect(response.toPromise()).resolves.toBe(mockResponse); }); - test('Observable should fail if fetch has an internal error', async () => { - const mockResponse: any = { result: 500, message: 'Internal Error' }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + describe('Should throw typed errors', () => { + test('Observable should fail if fetch has an internal error', async () => { + const mockResponse: any = new Error('Internal Error'); + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow('Internal Error'); + }); - try { - await response.toPromise(); - } catch (e) { - expect(e).toBe(mockResponse); - } - }); + describe('Should handle Timeout errors', () => { + test('Should throw SearchTimeoutError on server timeout AND show toast', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + }); - test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => { - const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, - }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + test('Timeout error should show multiple times if not in a session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; - try { - await response.toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(SearchTimeoutError); - expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); - done(); - } - }); + await expect(searchInterceptor.search(mockRequest).toPromise()).rejects.toThrow( + SearchTimeoutError + ); + await expect(searchInterceptor.search(mockRequest).toPromise()).rejects.toThrow( + SearchTimeoutError + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(2); + }); - test('Search error should be debounced', async (done) => { - const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, - }; - mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - try { - await searchInterceptor.search(mockRequest).toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(SearchTimeoutError); - try { - await searchInterceptor.search(mockRequest).toPromise(); - } catch (e2) { + test('Timeout error should show once per each session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'def' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(2); + }); + + test('Timeout error should show once in a single session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); - done(); - } - } - }); + }); + }); - test('Should throw Painless error on server error with OSS format', async (done) => { - const mockResponse: any = { - result: 500, - body: { - attributes: { - error: { - failed_shards: [ - { - reason: { - lang: 'painless', - script_stack: ['a', 'b'], - reason: 'banana', + test('Should throw Painless error on server error with OSS format', async () => { + const mockResponse: any = { + result: 500, + body: { + attributes: { + error: { + failed_shards: [ + { + reason: { + lang: 'painless', + script_stack: ['a', 'b'], + reason: 'banana', + }, }, - }, - ], + ], + }, }, }, - }, - }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(PainlessError); + }); - try { - await response.toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(PainlessError); - done(); - } - }); + test('Observable should fail if user aborts (test merged signal)', async () => { + const abortController = new AbortController(); + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { + return new Promise((resolve, reject) => { + options.signal.addEventListener('abort', () => { + reject(new AbortError()); + }); - test('Observable should fail if user aborts (test merged signal)', async () => { - const abortController = new AbortController(); - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); + setTimeout(resolve, 500); }); - - setTimeout(resolve, 500); }); - }); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest, { - abortSignal: abortController.signal, - }); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest, { + abortSignal: abortController.signal, + }); - const next = jest.fn(); - const error = (e: any) => { - expect(next).not.toBeCalled(); - expect(e).toBeInstanceOf(AbortError); - }; - response.subscribe({ next, error }); - setTimeout(() => abortController.abort(), 200); - jest.advanceTimersByTime(5000); + const next = jest.fn(); + const error = (e: any) => { + expect(next).not.toBeCalled(); + expect(e).toBeInstanceOf(AbortError); + }; + response.subscribe({ next, error }); + setTimeout(() => abortController.abort(), 200); + jest.advanceTimersByTime(5000); - await flushPromises(); - }); + await flushPromises(); + }); - test('Immediately aborts if passed an aborted abort signal', async (done) => { - const abort = new AbortController(); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); - abort.abort(); + test('Immediately aborts if passed an aborted abort signal', async (done) => { + const abort = new AbortController(); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); + abort.abort(); - const error = (e: any) => { - expect(e).toBeInstanceOf(AbortError); - expect(mockCoreSetup.http.fetch).not.toBeCalled(); - done(); - }; - response.subscribe({ error }); + const error = (e: any) => { + expect(e).toBeInstanceOf(AbortError); + expect(mockCoreSetup.http.fetch).not.toBeCalled(); + done(); + }; + response.subscribe({ error }); + }); }); }); }); diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 2e42635a7f811..e3c6dd3e287d4 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -17,7 +17,7 @@ * under the License. */ -import { get, trimEnd, debounce } from 'lodash'; +import { get, memoize, trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; @@ -28,6 +28,7 @@ import { IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, + ISessionService, } from '../../common'; import { SearchUsageCollector } from './collectors'; import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors'; @@ -39,6 +40,7 @@ export interface SearchInterceptorDeps { startServices: Promise<[CoreStart, any, unknown]>; toasts: ToastsSetup; usageCollector?: SearchUsageCollector; + session: ISessionService; } export class SearchInterceptor { @@ -86,16 +88,17 @@ export class SearchInterceptor { e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, - appAbortSignal?: AbortSignal + options?: ISearchOptions ): Error { if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') { // Handle a client or a server side timeout const err = new SearchTimeoutError(e, this.getTimeoutMode()); // Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors. - this.showTimeoutError(err); + // The timeout error is shown any time a request times out, or once per session, if the request is part of a session. + this.showTimeoutError(err, options?.sessionId); return err; - } else if (appAbortSignal?.aborted) { + } else if (options?.abortSignal?.aborted) { // In the case an application initiated abort, throw the existing AbortError. return e; } else if (isPainlessError(e)) { @@ -162,27 +165,37 @@ export class SearchInterceptor { combinedSignal.addEventListener('abort', cleanup); return { - combinedSignal, timeoutSignal, + combinedSignal, cleanup, }; } + private showTimeoutErrorToast = (e: SearchTimeoutError, sessionId?: string) => { + this.deps.toasts.addDanger({ + title: 'Timed out', + text: toMountPoint(e.getErrorMessage(this.application)), + }); + }; + + private showTimeoutErrorMemoized = memoize( + this.showTimeoutErrorToast, + (_: SearchTimeoutError, sessionId: string) => { + return sessionId; + } + ); + /** - * Right now we are throttling but we will hook this up with background sessions to show only one - * error notification per session. + * Show one error notification per session. * @internal */ - private showTimeoutError = debounce( - (e: SearchTimeoutError) => { - this.deps.toasts.addDanger({ - title: 'Timed out', - text: toMountPoint(e.getErrorMessage(this.application)), - }); - }, - 30000, - { leading: true, trailing: false } - ); + private showTimeoutError = (e: SearchTimeoutError, sessionId?: string) => { + if (sessionId) { + this.showTimeoutErrorMemoized(e, sessionId); + } else { + this.showTimeoutErrorToast(e, sessionId); + } + }; /** * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort @@ -207,12 +220,9 @@ export class SearchInterceptor { abortSignal: options?.abortSignal, }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return this.runSearch(request, combinedSignal, options?.strategy).pipe( - catchError((e: any) => { - return throwError( - this.handleSearchError(e, request, timeoutSignal, options?.abortSignal) - ); + catchError((e: Error) => { + return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 734e88e085661..f955dc5b6ebd5 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -31,6 +31,7 @@ import { ISearchOptions, SearchSourceService, SearchSourceDependencies, + ISessionService, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -40,6 +41,7 @@ import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; +import { SessionService } from './session_service'; import { ConfigSchema } from '../../config'; import { SHARD_DELAY_AGG_NAME, @@ -64,6 +66,7 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; + private sessionService!: ISessionService; constructor(private initializerContext: PluginInitializerContext) {} @@ -73,6 +76,7 @@ export class SearchService implements Plugin { ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); + this.sessionService = new SessionService(this.initializerContext, getStartServices); /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -83,6 +87,7 @@ export class SearchService implements Plugin { uiSettings, startServices: getStartServices(), usageCollector: this.usageCollector!, + session: this.sessionService, }); expressions.registerFunction(esdsl); @@ -104,6 +109,7 @@ export class SearchService implements Plugin { __enhance: (enhancements: SearchEnhancements) => { this.searchInterceptor = enhancements.searchInterceptor; }, + session: this.sessionService, }; } @@ -142,6 +148,7 @@ export class SearchService implements Plugin { showError: (e: Error) => { this.searchInterceptor.showError(e); }, + session: this.sessionService, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), }; } diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session_service.test.ts new file mode 100644 index 0000000000000..dd64d187f47d6 --- /dev/null +++ b/src/plugins/data/public/search/session_service.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SessionService } from './session_service'; +import { ISessionService } from '../../common'; +import { coreMock } from '../../../../core/public/mocks'; + +describe('Session service', () => { + let sessionService: ISessionService; + + beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext(); + sessionService = new SessionService( + initializerContext, + coreMock.createSetup().getStartServices + ); + }); + + describe('Session management', () => { + it('Creates and clears a session', async () => { + sessionService.start(); + expect(sessionService.getSessionId()).not.toBeUndefined(); + sessionService.clear(); + expect(sessionService.getSessionId()).toBeUndefined(); + }); + }); +}); diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts new file mode 100644 index 0000000000000..31524434af302 --- /dev/null +++ b/src/plugins/data/public/search/session_service.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import uuid from 'uuid'; +import { Subject, Subscription } from 'rxjs'; +import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { ISessionService } from '../../common/search'; +import { ConfigSchema } from '../../config'; + +export class SessionService implements ISessionService { + private sessionId?: string; + private session$: Subject = new Subject(); + private appChangeSubscription$?: Subscription; + private curApp?: string; + + constructor( + initializerContext: PluginInitializerContext, + getStartServices: StartServicesAccessor + ) { + /* + Make sure that apps don't leave sessions open. + */ + getStartServices().then(([coreStart]) => { + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { + if (this.sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + } + } + this.curApp = appName; + }); + }); + } + + public destroy() { + this.appChangeSubscription$?.unsubscribe(); + } + + public getSessionId() { + return this.sessionId; + } + + public getSession$() { + return this.session$.asObservable(); + } + + public start() { + this.sessionId = uuid.v4(); + this.session$.next(this.sessionId); + return this.sessionId; + } + + public clear() { + this.sessionId = undefined; + this.session$.next(this.sessionId); + } +} diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 85ef7aa4d97cb..c08d9f4c7be3f 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,7 +21,7 @@ import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; +import { ISearchGeneric, ISessionService, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; @@ -38,6 +38,11 @@ export interface SearchEnhancements { export interface ISearchSetup { aggs: AggsSetup; usageCollector?: SearchUsageCollector; + /** + * session management + * {@link ISessionService} + */ + session: ISessionService; /** * @internal */ @@ -67,6 +72,11 @@ export interface ISearchStart { * {@link ISearchStartSearchSource} */ searchSource: ISearchStartSearchSource; + /** + * session management + * {@link ISessionService} + */ + session: ISessionService; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 0828460830f2c..0ed296a1d0662 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -685,6 +685,7 @@ export class IndexPatternsService implements Plugin_3 { await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickKibanaIndexPatterns(); + const exists = await this.hasIndexPattern(indexPatternName); + + if (exists) { + await this.clickIndexPatternByName(indexPatternName); + return; + } + await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickAddNewIndexPatternButton(); if (!isStandardIndexPattern) { diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index f5416a44e3b5a..1148da14556e2 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -71,6 +71,12 @@ export function ToastsProvider({ getService }: FtrProviderContext) { private async getGlobalToastList() { return await testSubjects.find('globalToastList'); } + + public async getToastCount() { + const list = await this.getGlobalToastList(); + const toasts = await list.findAllByCssSelector(`.euiToast`); + return toasts.length; + } } return new Toasts(); diff --git a/test/plugin_functional/plugins/session_notifications/kibana.json b/test/plugin_functional/plugins/session_notifications/kibana.json new file mode 100644 index 0000000000000..0b80b531d2f84 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "session_notifications", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["session_notifications"], + "server": false, + "ui": true, + "requiredPlugins": ["data", "navigation"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/session_notifications/package.json b/test/plugin_functional/plugins/session_notifications/package.json new file mode 100644 index 0000000000000..7a61867db2b58 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/package.json @@ -0,0 +1,18 @@ +{ + "name": "session_notifications", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/session_notifications", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} + diff --git a/test/plugin_functional/plugins/session_notifications/public/index.ts b/test/plugin_functional/plugins/session_notifications/public/index.ts new file mode 100644 index 0000000000000..fbc573e8dc6fe --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { SessionNotificationsPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new SessionNotificationsPlugin(); diff --git a/test/plugin_functional/plugins/session_notifications/public/plugin.tsx b/test/plugin_functional/plugins/session_notifications/public/plugin.tsx new file mode 100644 index 0000000000000..a41ecb3edebd2 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/plugin.tsx @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { AppPluginDependenciesStart, AppPluginDependenciesSetup } from './types'; + +export class SessionNotificationsPlugin implements Plugin { + private sessionIds: Array = []; + public setup(core: CoreSetup, { navigation }: AppPluginDependenciesSetup) { + const showSessions = { + id: 'showSessionsButton', + label: 'Show Sessions', + description: 'Sessions', + run: () => { + core.notifications.toasts.addInfo(this.sessionIds.join(','), { + toastLifeTimeMs: 50000, + }); + }, + tooltip: () => { + return this.sessionIds.join(','); + }, + testId: 'showSessionsButton', + }; + + navigation.registerMenuItem(showSessions); + + const clearSessions = { + id: 'clearSessionsButton', + label: 'Clear Sessions', + description: 'Sessions', + run: () => { + this.sessionIds.length = 0; + }, + testId: 'clearSessionsButton', + }; + + navigation.registerMenuItem(clearSessions); + } + + public start(core: CoreStart, { data }: AppPluginDependenciesStart) { + core.application.currentAppId$.subscribe(() => { + this.sessionIds.length = 0; + }); + + data.search.session.getSession$().subscribe((sessionId?: string) => { + this.sessionIds.push(sessionId); + }); + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/session_notifications/public/types.ts b/test/plugin_functional/plugins/session_notifications/public/types.ts new file mode 100644 index 0000000000000..de9055d03d21e --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/types.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { NavigationPublicPluginSetup } from '../../../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; + +export interface AppPluginDependenciesSetup { + navigation: NavigationPublicPluginSetup; +} +export interface AppPluginDependenciesStart { + data: DataPublicPluginStart; +} diff --git a/test/plugin_functional/plugins/session_notifications/tsconfig.json b/test/plugin_functional/plugins/session_notifications/tsconfig.json new file mode 100644 index 0000000000000..3d9d8ca9451d4 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/data_plugin/index.ts b/test/plugin_functional/test_suites/data_plugin/index.ts index bbf9d823e357e..212a75b9cf441 100644 --- a/test/plugin_functional/test_suites/data_plugin/index.ts +++ b/test/plugin_functional/test_suites/data_plugin/index.ts @@ -35,7 +35,9 @@ export default function ({ await PageObjects.common.navigateToApp('settings'); await PageObjects.settings.createIndexPattern('shakespeare', ''); }); - loadTestFile(require.resolve('./index_patterns')); + loadTestFile(require.resolve('./search')); + loadTestFile(require.resolve('./session')); + loadTestFile(require.resolve('./index_patterns')); }); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 4359816efb958..2c846dc780311 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -23,7 +23,9 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); - describe('index patterns', function () { + // skipping the tests as it deletes index patterns created by other test causing unexpected failures + // https://github.com/elastic/kibana/issues/79886 + describe.skip('index patterns', function () { let indexPatternId = ''; it('can get all ids', async () => { diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts new file mode 100644 index 0000000000000..88241fffae904 --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'discover', 'timePicker']); + const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); + + const getSessionIds = async () => { + const sessionsBtn = await testSubjects.find('showSessionsButton'); + await sessionsBtn.click(); + const toast = await toasts.getToastElement(1); + const sessionIds = await toast.getVisibleText(); + return sessionIds.split(','); + }; + + describe('Session management', function describeIndexTests() { + describe('Discover', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.click('clearSessionsButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + afterEach(async () => { + await testSubjects.click('clearSessionsButton'); + await toasts.dismissAllToasts(); + }); + + it('Starts on index pattern select', async () => { + await PageObjects.discover.selectIndexPattern('shakespeare'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + + // Discover calls destroy on index pattern change, which explicitly closes a session + expect(sessionIds.length).to.be(2); + expect(sessionIds[0].length).to.be(0); + expect(sessionIds[1].length).not.to.be(0); + }); + + it('Starts on a refresh', async () => { + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('Starts a new session on sort', async () => { + await PageObjects.discover.clickFieldListItemAdd('speaker'); + await PageObjects.discover.clickFieldSort('speaker'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('Starts a new session on filter change', async () => { + await filterBar.addFilter('line_number', 'is', '4.3.108'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + }); + }); +} diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index ccc93316482c2..43ad4a9ed9b8b 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -40,6 +40,7 @@ export class DataEnhancedPlugin uiSettings: core.uiSettings, startServices: core.getStartServices(), usageCollector: data.search.usageCollector, + session: data.search.session, }); data.__enhance({ diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 6e34e4c1964c5..3187b41a2c55f 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -9,6 +9,7 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { SearchTimeoutError } from 'src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -43,6 +44,7 @@ describe('EnhancedSearchInterceptor', () => { beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + const dataPluginMockStart = dataPluginMock.createStartContract(); mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { switch (name) { @@ -77,6 +79,7 @@ describe('EnhancedSearchInterceptor', () => { http: mockCoreSetup.http, uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, + session: dataPluginMockStart.search.session, }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index cca87c85e326c..aee32a7c62759 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -98,7 +98,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { if (id !== undefined) { this.deps.http.delete(`/internal/search/${strategy}/${id}`); } - return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)); + return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 47da1e93cf004..bfc104b105236 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -21,6 +21,7 @@ import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../../common/ecs'; import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ISearchStart } from '../../../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; jest.mock('apollo-client'); @@ -29,7 +30,7 @@ describe('alert actions', () => { const unix = moment(anchor).valueOf(); let createTimeline: CreateTimeline; let updateTimelineIsLoading: UpdateTimelineLoading; - let searchStrategyClient: ISearchStart; + let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; beforeEach(() => { @@ -42,11 +43,13 @@ describe('alert actions', () => { createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; + searchStrategyClient = { aggs: {} as ISearchStart['aggs'], showError: jest.fn(), search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }), searchSource: {} as ISearchStart['searchSource'], + session: dataPluginMock.createStartContract().search.session, }; jest.spyOn(apolloClient, 'query').mockImplementation((obj) => { From 2e37bd070358fa955bc669f9e33d42db4be7f995 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 16 Oct 2020 12:55:46 +0300 Subject: [PATCH 40/79] Add script to identify plugin dependencies for TS project references migration (#80463) * move kbn-dev-utils plugin helpers under a dedicated folder * use getPluginSearchPaths in kbn-config & kbn-optimizer * add a script to find plugin dependencies not migrated to TS project refs * update docs * add a script reporting all circular deps between plugins based on kibana.json declaration, so it doesn't provide all the cases * fix optimizer scan logic. removed by mistake * revert changes. fails on CI * remove prod depenedency on kbn/dev-utils * remove last export * only run plugin discovery once to speed up circular dep detection * address comments * address comments * update fixtures Co-authored-by: spalger --- .../best-practices/typescript.asciidoc | 2 +- packages/kbn-config/src/env.ts | 21 ++-- packages/kbn-config/src/index.ts | 1 + packages/kbn-config/src/plugins/index.ts | 19 ++++ .../src/plugins/plugin_search_paths.ts | 36 +++++++ packages/kbn-dev-utils/src/index.ts | 3 +- .../src/plugin_list/discover_plugins.ts | 2 +- packages/kbn-dev-utils/src/plugins/index.ts | 21 ++++ .../parse_kibana_platform_plugin.ts | 39 ++++++-- ...simple_kibana_platform_plugin_discovery.ts | 0 packages/kbn-optimizer/package.json | 1 + .../mock_repo/plugins/bar/kibana.json | 3 +- .../mock_repo/plugins/foo/kibana.json | 3 +- .../mock_repo/plugins/nested/baz/kibana.json | 3 +- .../test_plugins/test_baz/kibana.json | 3 +- .../mock_repo/x-pack/baz/kibana.json | 3 +- .../src/optimizer/optimizer_config.ts | 22 ++--- scripts/find_plugin_circular_deps.js | 21 ++++ scripts/find_plugins_without_ts_refs.js | 21 ++++ src/dev/plugin_discovery/find_plugins.ts | 50 ++++++++++ src/dev/plugin_discovery/get_plugin_deps.ts | 89 +++++++++++++++++ src/dev/plugin_discovery/index.ts | 21 ++++ src/dev/run_find_plugin_circular_deps.ts | 73 ++++++++++++++ src/dev/run_find_plugins_without_ts_refs.ts | 95 +++++++++++++++++++ 24 files changed, 508 insertions(+), 44 deletions(-) create mode 100644 packages/kbn-config/src/plugins/index.ts create mode 100644 packages/kbn-config/src/plugins/plugin_search_paths.ts create mode 100644 packages/kbn-dev-utils/src/plugins/index.ts rename packages/kbn-dev-utils/src/{ => plugins}/parse_kibana_platform_plugin.ts (56%) rename packages/kbn-dev-utils/src/{ => plugins}/simple_kibana_platform_plugin_discovery.ts (100%) create mode 100644 scripts/find_plugin_circular_deps.js create mode 100644 scripts/find_plugins_without_ts_refs.js create mode 100644 src/dev/plugin_discovery/find_plugins.ts create mode 100644 src/dev/plugin_discovery/get_plugin_deps.ts create mode 100644 src/dev/plugin_discovery/index.ts create mode 100644 src/dev/run_find_plugin_circular_deps.ts create mode 100644 src/dev/run_find_plugins_without_ts_refs.ts diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index a2cda1e0b1e87..583a98f296de5 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -28,7 +28,7 @@ This architecture imposes several limitations to which we must comply: [discrete] ==== Prerequisites Since project refs rely on generated `d.ts` files, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. -https://github.com/elastic/kibana/issues/79343 is going to provide a tool for identifying a plugin dependency tree. +Run `node scripts/find_plugins_without_ts_refs.js --id your_plugin_id` to get a list of plugins that should be switched to TS project refs to unblock your plugin migration. [discrete] ==== Implementation diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index e4585056696f9..e7b4658262235 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -19,6 +19,7 @@ import { resolve, join } from 'path'; import loadJsonFile from 'load-json-file'; +import { getPluginSearchPaths } from './plugins'; import { PackageInfo, EnvironmentMode } from './types'; /** @internal */ @@ -114,21 +115,11 @@ export class Env { this.binDir = resolve(this.homeDir, 'bin'); this.logDir = resolve(this.homeDir, 'log'); - /** - * BEWARE: this needs to stay roughly synchronized with the @kbn/optimizer - * `packages/kbn-optimizer/src/optimizer_config.ts` determines the paths - * that should be searched for plugins to build - */ - this.pluginSearchPaths = [ - resolve(this.homeDir, 'src', 'plugins'), - ...(options.cliArgs.oss ? [] : [resolve(this.homeDir, 'x-pack', 'plugins')]), - resolve(this.homeDir, 'plugins'), - ...(options.cliArgs.runExamples ? [resolve(this.homeDir, 'examples')] : []), - ...(options.cliArgs.runExamples && !options.cliArgs.oss - ? [resolve(this.homeDir, 'x-pack', 'examples')] - : []), - resolve(this.homeDir, '..', 'kibana-extra'), - ]; + this.pluginSearchPaths = getPluginSearchPaths({ + rootDir: this.homeDir, + oss: options.cliArgs.oss, + examples: options.cliArgs.runExamples, + }); this.cliArgs = Object.freeze(options.cliArgs); this.configs = Object.freeze(options.configs); diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index f02514a92e606..68609c6d5c7c3 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -35,3 +35,4 @@ export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env, RawPackageInfo } from './env'; export { EnvironmentMode, PackageInfo } from './types'; export { LegacyObjectToConfigAdapter, LegacyLoggingConfig } from './legacy'; +export { getPluginSearchPaths } from './plugins'; diff --git a/packages/kbn-config/src/plugins/index.ts b/packages/kbn-config/src/plugins/index.ts new file mode 100644 index 0000000000000..7d02f9fb984c2 --- /dev/null +++ b/packages/kbn-config/src/plugins/index.ts @@ -0,0 +1,19 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { getPluginSearchPaths } from './plugin_search_paths'; diff --git a/packages/kbn-config/src/plugins/plugin_search_paths.ts b/packages/kbn-config/src/plugins/plugin_search_paths.ts new file mode 100644 index 0000000000000..a7d151c3275c8 --- /dev/null +++ b/packages/kbn-config/src/plugins/plugin_search_paths.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { resolve } from 'path'; + +interface SearchOptions { + rootDir: string; + oss: boolean; + examples: boolean; +} + +export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions) { + return [ + resolve(rootDir, 'src', 'plugins'), + ...(oss ? [] : [resolve(rootDir, 'x-pack', 'plugins')]), + resolve(rootDir, 'plugins'), + ...(examples ? [resolve(rootDir, 'examples')] : []), + ...(examples && !oss ? [resolve(rootDir, 'x-pack', 'examples')] : []), + resolve(rootDir, '..', 'kibana-extra'), + ]; +} diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 6a845825f0fd4..98385b49dafa9 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -40,7 +40,6 @@ export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; -export * from './simple_kibana_platform_plugin_discovery'; +export * from './plugins'; export * from './streams'; export * from './babel'; -export * from './parse_kibana_platform_plugin'; diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index e8f6735205b19..9782067e61343 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -24,7 +24,7 @@ import MarkdownIt from 'markdown-it'; import cheerio from 'cheerio'; import { REPO_ROOT } from '@kbn/utils'; -import { simpleKibanaPlatformPluginDiscovery } from '../simple_kibana_platform_plugin_discovery'; +import { simpleKibanaPlatformPluginDiscovery } from '../plugins'; import { extractAsciidocInfo } from './extract_asciidoc_info'; export interface Plugin { diff --git a/packages/kbn-dev-utils/src/plugins/index.ts b/packages/kbn-dev-utils/src/plugins/index.ts new file mode 100644 index 0000000000000..8705682f355c7 --- /dev/null +++ b/packages/kbn-dev-utils/src/plugins/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './parse_kibana_platform_plugin'; +export * from './simple_kibana_platform_plugin_discovery'; diff --git a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts similarity index 56% rename from packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts rename to packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts index 83d8c2684d7ca..16aaecb3e478d 100644 --- a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts +++ b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts @@ -23,12 +23,27 @@ import loadJsonFile from 'load-json-file'; export interface KibanaPlatformPlugin { readonly directory: string; readonly manifestPath: string; - readonly manifest: { - id: string; - ui: boolean; - server: boolean; - [key: string]: unknown; - }; + readonly manifest: Manifest; +} + +function isValidDepsDeclaration(input: unknown, type: string): string[] { + if (typeof input === 'undefined') return []; + if (Array.isArray(input) && input.every((i) => typeof i === 'string')) { + return input; + } + throw new TypeError(`The "${type}" in plugin manifest should be an array of strings.`); +} + +interface Manifest { + id: string; + ui: boolean; + server: boolean; + kibanaVersion: string; + version: string; + requiredPlugins: readonly string[]; + optionalPlugins: readonly string[]; + requiredBundles: readonly string[]; + extraPublicDirs: readonly string[]; } export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { @@ -36,7 +51,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform manifest path to be absolute'); } - const manifest = loadJsonFile.sync(manifestPath); + const manifest: Partial = loadJsonFile.sync(manifestPath); if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); } @@ -45,6 +60,10 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform plugin manifest to have a string id'); } + if (typeof manifest.version !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string version'); + } + return { directory: Path.dirname(manifestPath), manifestPath, @@ -54,6 +73,12 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP ui: !!manifest.ui, server: !!manifest.server, id: manifest.id, + version: manifest.version, + kibanaVersion: manifest.kibanaVersion || manifest.version, + requiredPlugins: isValidDepsDeclaration(manifest.requiredPlugins, 'requiredPlugins'), + optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'), + requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'), + extraPublicDirs: isValidDepsDeclaration(manifest.extraPublicDirs, 'extraPublicDirs'), }, }; } diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts similarity index 100% rename from packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts rename to packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c9e414dbc5177..63146fc7a1834 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,6 +14,7 @@ "@babel/core": "^7.11.6", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "@kbn/config": "1.0.0", "@kbn/std": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "autoprefixer": "^9.7.4", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json index 33f53e336598d..a5e9f34a22aa6 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json @@ -1,5 +1,6 @@ { "id": "bar", "ui": true, - "requiredBundles": ["foo"] + "requiredBundles": ["foo"], + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json index 256856181ccd8..27730df199887 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json @@ -1,4 +1,5 @@ { "id": "foo", - "ui": true + "ui": true, + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json index 6e4e9c70a115c..a8f991ee11465 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json @@ -1,3 +1,4 @@ { - "id": "baz" + "id": "baz", + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json index b9e044523a6a5..d8a8b2e548e4a 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json @@ -1,3 +1,4 @@ { - "id": "test_baz" + "id": "test_baz", + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json index 10602d2e7981a..64ec7ff5ccf3e 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -1,4 +1,5 @@ { "id": "baz", - "ui": true + "ui": true, + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 8091f6aa90508..1443fccda04d8 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Os from 'os'; +import { getPluginSearchPaths } from '@kbn/config'; import { Bundle, @@ -167,19 +168,14 @@ export class OptimizerConfig { throw new TypeError('outputRoot must be an absolute path'); } - /** - * BEWARE: this needs to stay roughly synchronized with - * `src/core/server/config/env.ts` which determines which paths - * should be searched for plugins to load - */ - const pluginScanDirs = options.pluginScanDirs || [ - Path.resolve(repoRoot, 'src/plugins'), - ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), - Path.resolve(repoRoot, 'plugins'), - ...(examples ? [Path.resolve('examples')] : []), - ...(examples && !oss ? [Path.resolve('x-pack/examples')] : []), - Path.resolve(repoRoot, '../kibana-extra'), - ]; + const pluginScanDirs = + options.pluginScanDirs || + getPluginSearchPaths({ + rootDir: repoRoot, + oss, + examples, + }); + if (!pluginScanDirs.every((p) => Path.isAbsolute(p))) { throw new TypeError('pluginScanDirs must all be absolute paths'); } diff --git a/scripts/find_plugin_circular_deps.js b/scripts/find_plugin_circular_deps.js new file mode 100644 index 0000000000000..6b0661cb841b4 --- /dev/null +++ b/scripts/find_plugin_circular_deps.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_find_plugin_circular_deps'); diff --git a/scripts/find_plugins_without_ts_refs.js b/scripts/find_plugins_without_ts_refs.js new file mode 100644 index 0000000000000..5f543a045f739 --- /dev/null +++ b/scripts/find_plugins_without_ts_refs.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_find_plugins_without_ts_refs'); diff --git a/src/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts new file mode 100644 index 0000000000000..4e7c34698c964 --- /dev/null +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import Path from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getPluginSearchPaths } from '@kbn/config'; +import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; + +export interface SearchOptions { + oss: boolean; + examples: boolean; + extraPluginScanDirs: string[]; +} + +export function findPlugins({ + oss, + examples, + extraPluginScanDirs, +}: SearchOptions): Map { + const pluginSearchPaths = getPluginSearchPaths({ + rootDir: REPO_ROOT, + oss, + examples, + }); + + for (const extraScanDir of extraPluginScanDirs) { + if (!Path.isAbsolute(extraScanDir)) { + throw new TypeError('extraPluginScanDirs must all be absolute paths'); + } + pluginSearchPaths.push(extraScanDir); + } + + const plugins = simpleKibanaPlatformPluginDiscovery(pluginSearchPaths, []); + return new Map(plugins.map((p) => [p.manifest.id, p])); +} diff --git a/src/dev/plugin_discovery/get_plugin_deps.ts b/src/dev/plugin_discovery/get_plugin_deps.ts new file mode 100644 index 0000000000000..498feefd97094 --- /dev/null +++ b/src/dev/plugin_discovery/get_plugin_deps.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { KibanaPlatformPlugin } from '@kbn/dev-utils'; + +interface AllOptions { + id: string; + pluginMap: Map; +} + +interface CircularRefsError { + from: string; + to: string; + stack: string[]; +} + +export type SearchErrors = CircularRefsError; + +interface State { + deps: Set; + stack: string[]; + errors: Map; +} + +function traverse(pluginMap: Map, state: State, id: string) { + const plugin = pluginMap.get(id); + if (plugin === undefined) { + throw new Error(`Unknown plugin id: ${id}`); + } + + const prevIndex = state.stack.indexOf(id); + const isVisited = prevIndex > -1; + if (isVisited) { + const from = state.stack[state.stack.length - 1]; + const to = id; + const key = `circular-${[from, to].sort().join('-')}`; + + if (!state.errors.has(key)) { + const error: CircularRefsError = { + from, + to, + // provide sub-stack with circular refs only + stack: state.stack.slice(prevIndex), + }; + state.errors.set(key, error); + } + + return; + } + + state.stack.push(id); + new Set([ + ...plugin.manifest.requiredPlugins, + ...plugin.manifest.optionalPlugins, + ...plugin.manifest.requiredBundles, + ]).forEach((depId) => { + state.deps.add(pluginMap.get(depId)!); + traverse(pluginMap, state, depId); + }); + + state.stack.pop(); +} + +export function getPluginDeps({ pluginMap, id }: AllOptions): State { + const state: State = { + deps: new Set(), + errors: new Map(), + stack: [], + }; + + traverse(pluginMap, state, id); + + return state; +} diff --git a/src/dev/plugin_discovery/index.ts b/src/dev/plugin_discovery/index.ts new file mode 100644 index 0000000000000..4a4be65dfaef0 --- /dev/null +++ b/src/dev/plugin_discovery/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './find_plugins'; +export * from './get_plugin_deps'; diff --git a/src/dev/run_find_plugin_circular_deps.ts b/src/dev/run_find_plugin_circular_deps.ts new file mode 100644 index 0000000000000..501e2c4fed048 --- /dev/null +++ b/src/dev/run_find_plugin_circular_deps.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { run } from '@kbn/dev-utils'; +import { findPlugins, getPluginDeps, SearchErrors } from './plugin_discovery'; + +interface AllOptions { + examples?: boolean; + extraPluginScanDirs?: string[]; +} + +run( + async ({ flags, log }) => { + const { examples = false, extraPluginScanDirs = [] } = flags as AllOptions; + + const pluginMap = findPlugins({ + oss: false, + examples, + extraPluginScanDirs, + }); + + const allErrors = new Map(); + for (const pluginId of pluginMap.keys()) { + const { errors } = getPluginDeps({ + pluginMap, + id: pluginId, + }); + + for (const [errorId, error] of errors) { + if (!allErrors.has(errorId)) { + allErrors.set(errorId, error); + } + } + } + + if (allErrors.size > 0) { + allErrors.forEach((error) => { + log.warning( + `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` + ); + }); + } + }, + { + flags: { + boolean: ['examples'], + default: { + examples: false, + }, + allowUnexpected: false, + help: ` + --examples Include examples folder + --extraPluginScanDirs Include extra scan folder + `, + }, + } +); diff --git a/src/dev/run_find_plugins_without_ts_refs.ts b/src/dev/run_find_plugins_without_ts_refs.ts new file mode 100644 index 0000000000000..ad63884671e24 --- /dev/null +++ b/src/dev/run_find_plugins_without_ts_refs.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; +import Fs from 'fs'; +import { get } from 'lodash'; +import { run } from '@kbn/dev-utils'; +import { getPluginDeps, findPlugins } from './plugin_discovery'; + +interface AllOptions { + id?: string; + examples?: boolean; + extraPluginScanDirs?: string[]; +} + +run( + async ({ flags, log }) => { + const { examples = false, extraPluginScanDirs = [], id } = flags as AllOptions; + + if (!id) { + throw new Error('Plugin id required'); + } + + const pluginMap = findPlugins({ + oss: false, + examples, + extraPluginScanDirs, + }); + + const result = getPluginDeps({ + pluginMap, + id, + }); + + if (result.errors.size > 0) { + result.errors.forEach((error) => { + log.warning( + `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` + ); + }); + } + + const notMigratedPlugins = [...result.deps].filter( + (plugin) => !isMigratedToTsProjectRefs(plugin.directory) + ); + if (notMigratedPlugins.length > 0) { + log.info( + `Dependencies haven't been migrated to TS project refs yet:\n${notMigratedPlugins + .map((p) => p.manifest.id) + .join('\n')}` + ); + } + }, + { + flags: { + boolean: ['examples'], + string: ['id'], + default: { + examples: false, + }, + allowUnexpected: false, + help: ` + --id Plugin id to perform deps search for + --examples Include examples folder + --extraPluginScanDirs Include extra scan folder + `, + }, + } +); + +function isMigratedToTsProjectRefs(dir: string): boolean { + try { + const path = Path.join(dir, 'tsconfig.json'); + const content = Fs.readFileSync(path, { encoding: 'utf8' }); + return get(JSON.parse(content), 'compilerOptions.composite', false); + } catch (e) { + return false; + } +} From 51ac14ba57097ede9e323ed8595a79c211baba84 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 16 Oct 2020 07:21:57 -0400 Subject: [PATCH 41/79] Allow the default space to be accessed via `/s/default` (#77109) * Allow the default space to be accessed via /s/default * apply suggestions from code review --- .../common/lib/spaces_url_parser.test.ts | 53 ++++++++++-- .../spaces/common/lib/spaces_url_parser.ts | 28 ++++-- .../on_request_interceptor.ts | 5 +- .../spaces_service/spaces_service.test.ts | 2 +- .../server/spaces_service/spaces_service.ts | 2 +- .../apis/spaces/get_active_space.ts | 14 +++ .../common/lib/space_test_utils.ts | 21 +++++ .../common/suites/create.ts | 86 ++++++++++--------- .../common/suites/delete.ts | 50 +++++------ .../common/suites/get.ts | 16 ++-- .../common/suites/get_all.ts | 50 +++++------ 11 files changed, 208 insertions(+), 119 deletions(-) diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts index b25d79c0a6907..2b34bc77ec686 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts @@ -10,41 +10,76 @@ describe('getSpaceIdFromPath', () => { describe('without a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath)).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); + }); + + test('it identifies the space url context with the default space', () => { + const basePath = `/s/${DEFAULT_SPACE_ID}`; + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: true, + }); }); }); describe('with a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/')).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath, '/')).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('it identifies the space url context following the server base path', () => { const basePath = `/server-base-path-here/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual( - 'my-awesome-space-lives-here' - ); + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); + }); + + test('it identifies the space url context with the default space following the server base path', () => { + const basePath = `/server-base-path-here/s/${DEFAULT_SPACE_ID}`; + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: true, + }); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceIdFromPath(basePath, basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath, basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); }); }); }); diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index 994ec7c59cb6e..be950e6a651e6 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -5,20 +5,22 @@ */ import { DEFAULT_SPACE_ID } from '../constants'; +const spaceContextRegex = /^\/s\/([a-z0-9_\-]+)/; + export function getSpaceIdFromPath( requestBasePath: string = '/', serverBasePath: string = '/' -): string { - let pathToCheck: string = requestBasePath; +): { spaceId: string; pathHasExplicitSpaceIdentifier: boolean } { + const pathToCheck: string = stripServerBasePath(requestBasePath, serverBasePath); - if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { - pathToCheck = requestBasePath.substr(serverBasePath.length); - } // Look for `/s/space-url-context` in the base path - const matchResult = pathToCheck.match(/^\/s\/([a-z0-9_\-]+)/); + const matchResult = pathToCheck.match(spaceContextRegex); if (!matchResult || matchResult.length === 0) { - return DEFAULT_SPACE_ID; + return { + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }; } // Ignoring first result, we only want the capture group result at index 1 @@ -28,7 +30,10 @@ export function getSpaceIdFromPath( throw new Error(`Unable to determine Space ID from request path: ${requestBasePath}`); } - return spaceId; + return { + spaceId, + pathHasExplicitSpaceIdentifier: true, + }; } export function addSpaceIdToPath( @@ -45,3 +50,10 @@ export function addSpaceIdToPath( } return `${basePath}${requestedPath}`; } + +function stripServerBasePath(requestBasePath: string, serverBasePath: string) { + if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { + return requestBasePath.substr(serverBasePath.length); + } + return requestBasePath; +} diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 4b3a5d662f12d..6408803c2114b 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -10,7 +10,6 @@ import { CoreSetup, } from 'src/core/server'; import { format } from 'url'; -import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { modifyUrl } from '../utils/url'; import { getSpaceIdFromPath } from '../../../common'; @@ -28,9 +27,9 @@ export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDep // If navigating within the context of a space, then we store the Space's URL Context on the request, // and rewrite the request to not include the space identifier in the URL. - const spaceId = getSpaceIdFromPath(path, serverBasePath); + const { spaceId, pathHasExplicitSpaceIdentifier } = getSpaceIdFromPath(path, serverBasePath); - if (spaceId !== DEFAULT_SPACE_ID) { + if (pathHasExplicitSpaceIdentifier) { const reqBasePath = `/s/${spaceId}`; http.basePath.set(request, reqBasePath); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index b341d76c86649..b48bf971d0c1b 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -58,7 +58,7 @@ const createService = async (serverBasePath: string = '') => { serverBasePath, } as HttpServiceSetup['basePath']; httpSetup.basePath.get = jest.fn().mockImplementation((request: KibanaRequest) => { - const spaceId = getSpaceIdFromPath(request.url.path); + const { spaceId } = getSpaceIdFromPath(request.url.path); if (spaceId !== DEFAULT_SPACE_ID) { return `/s/${spaceId}`; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index cf181a78efcb8..3630675a7ed3f 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -63,7 +63,7 @@ export class SpacesService { ? (request as Record).getBasePath() : http.basePath.get(request); - const spaceId = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); + const { spaceId } = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); return spaceId; }; diff --git a/x-pack/test/api_integration/apis/spaces/get_active_space.ts b/x-pack/test/api_integration/apis/spaces/get_active_space.ts index b925df3918825..16cb03fe8a316 100644 --- a/x-pack/test/api_integration/apis/spaces/get_active_space.ts +++ b/x-pack/test/api_integration/apis/spaces/get_active_space.ts @@ -35,6 +35,20 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('returns the default space when explicitly referenced', async () => { + await supertest + .get('/s/default/internal/spaces/_active_space') + .set('kbn-xsrf', 'xxx') + .expect(200, { + id: 'default', + name: 'Default', + description: 'This is your default space!', + color: '#00bfb3', + disabledFeatures: [], + _reserved: true, + }); + }); + it('returns the foo space', async () => { await supertest .get('/s/foo-space/internal/spaces/_active_space') diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index f233bc1d11d7c..c8e13f6bada7a 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -13,3 +13,24 @@ export function getUrlPrefix(spaceId?: string) { export function getIdPrefix(spaceId?: string) { return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } + +export function getTestScenariosForSpace(spaceId: string) { + const explicitScenario = { + spaceId, + urlPrefix: `/s/${spaceId}`, + scenario: `when referencing the ${spaceId} space explicitly in the URL`, + }; + + if (spaceId === DEFAULT_SPACE_ID) { + return [ + { + spaceId, + urlPrefix: ``, + scenario: 'when referencing the default space implicitly', + }, + explicitScenario, + ]; + } + + return [explicitScenario]; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 4de638c784147..7c2120ce6eeaf 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface CreateTest { @@ -67,56 +67,58 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest { describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.newSpace.statusCode}`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'marketing', - id: 'marketing', - description: 'a description', - color: '#5c5959', - disabledFeatures: [], - }) - .expect(tests.newSpace.statusCode) - .then(tests.newSpace.response); - }); - - describe('when it already exists', () => { - it(`should return ${tests.alreadyExists.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.newSpace.statusCode} ${scenario}`, async () => { return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .post(`${urlPrefix}/api/spaces/space`) .auth(user.username, user.password) .send({ - name: 'space_1', - id: 'space_1', - color: '#ffffff', + name: 'marketing', + id: 'marketing', description: 'a description', + color: '#5c5959', disabledFeatures: [], }) - .expect(tests.alreadyExists.statusCode) - .then(tests.alreadyExists.response); + .expect(tests.newSpace.statusCode) + .then(tests.newSpace.response); }); - }); - describe('when _reserved is specified', () => { - it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'reserved space', - id: 'reserved', - description: 'a description', - color: '#5c5959', - _reserved: true, - disabledFeatures: [], - }) - .expect(tests.reservedSpecified.statusCode) - .then(tests.reservedSpecified.response); + describe('when it already exists', () => { + it(`should return ${tests.alreadyExists.statusCode} ${scenario}`, async () => { + return supertest + .post(`${urlPrefix}/api/spaces/space`) + .auth(user.username, user.password) + .send({ + name: 'space_1', + id: 'space_1', + color: '#ffffff', + description: 'a description', + disabledFeatures: [], + }) + .expect(tests.alreadyExists.statusCode) + .then(tests.alreadyExists.response); + }); + }); + + describe('when _reserved is specified', () => { + it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved ${scenario}`, async () => { + return supertest + .post(`${urlPrefix}/api/spaces/space`) + .auth(user.username, user.password) + .send({ + name: 'reserved space', + id: 'reserved', + description: 'a description', + color: '#5c5959', + _reserved: true, + disabledFeatures: [], + }) + .expect(tests.reservedSpecified.statusCode) + .then(tests.reservedSpecified.response); + }); }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 69b5697d8a9a8..2a6b2c0e69d1d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; @@ -176,7 +176,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe { user = {}, spaceId, tests }: DeleteTestDefinition ) => { describeFn(description, () => { - before(async () => { + beforeEach(async () => { await esArchiver.load('saved_objects/spaces'); // since we want to verify that we only delete the right things @@ -189,33 +189,35 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe .auth(user.username, user.password) .expect(200); }); - after(() => esArchiver.unload('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.exists.statusCode}`, async () => { - return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_2`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe(`when the space is reserved`, () => { - it(`should return ${tests.reservedSpace.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/default`) + .delete(`${urlPrefix}/api/spaces/space/space_2`) .auth(user.username, user.password) - .expect(tests.reservedSpace.statusCode) - .then(tests.reservedSpace.response); + .expect(tests.exists.statusCode) + .then(tests.exists.response); }); - }); - describe(`when the space doesn't exist`, () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { - return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_3`) - .auth(user.username, user.password) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + describe(`when the space is reserved`, () => { + it(`should return ${tests.reservedSpace.statusCode} ${scenario}`, async () => { + return supertest + .delete(`${urlPrefix}/api/spaces/space/default`) + .auth(user.username, user.password) + .expect(tests.reservedSpace.statusCode) + .then(tests.reservedSpace.response); + }); + }); + + describe(`when the space doesn't exist`, () => { + it(`should return ${tests.doesntExist.statusCode} ${scenario}`, async () => { + return supertest + .delete(`${urlPrefix}/api/spaces/space/space_3`) + .auth(user.username, user.password) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index bd0e2a18d5c50..6bf5b0f180237 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperAgent } from 'superagent'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetTest { @@ -80,12 +80,14 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(currentSpaceId)}/api/spaces/space/${spaceId}`) - .auth(user.username, user.password) - .expect(tests.default.statusCode) - .then(tests.default.response); + getTestScenariosForSpace(currentSpaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.default.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space/${spaceId}`) + .auth(user.username, user.password) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); }); }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index d41d73bba90bc..fce48e4938baa 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetAllTest { @@ -71,33 +71,35 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.exists.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe('copySavedObjects purpose', () => { - it(`should return ${tests.copySavedObjectsPurpose.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ scenario, urlPrefix }) => { + it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .query({ purpose: 'copySavedObjectsIntoSpace' }) + .get(`${urlPrefix}/api/spaces/space`) .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); + .expect(tests.exists.statusCode) + .then(tests.exists.response); }); - }); - describe('copySavedObjects purpose', () => { - it(`should return ${tests.shareSavedObjectsPurpose.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .query({ purpose: 'shareSavedObjectsIntoSpace' }) - .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); + describe('copySavedObjects purpose', () => { + it(`should return ${tests.copySavedObjectsPurpose.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space`) + .query({ purpose: 'copySavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); + }); + + describe('copySavedObjects purpose', () => { + it(`should return ${tests.shareSavedObjectsPurpose.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space`) + .query({ purpose: 'shareSavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); }); }); }); From a377204f85dc3bbbb048dc315966c54707adff9d Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 16 Oct 2020 07:35:46 -0400 Subject: [PATCH 42/79] [Alerting UI] Disable "Save" button for Alerts with broken Connectors (#80579) * Adding check for broken connectors in action form * Adding check for broken connectors in action form * Adding unit test * PR fixes --- .../action_form.test.tsx | 49 ++++++++++++++----- .../action_connector_form/action_form.tsx | 12 +++++ .../sections/alert_form/alert_edit.tsx | 6 ++- .../sections/alert_form/alert_form.tsx | 3 ++ 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7ee1e0d3f3fa6..34569dcc75240 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -114,7 +114,7 @@ describe('action_form', () => { describe('action_form in alert', () => { let wrapper: ReactWrapper; - async function setup() { + async function setup(customActions?: AlertAction[]) { const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); loadAllActions.mockResolvedValueOnce([ { @@ -177,6 +177,7 @@ describe('action_form', () => { show: true, }, }, + setHasActionsWithBrokenConnector: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; @@ -198,16 +199,18 @@ describe('action_form', () => { schedule: { interval: '1m', }, - actions: [ - { - group: 'default', - id: 'test', - actionTypeId: actionType.id, - params: { - message: '', - }, - }, - ], + actions: customActions + ? customActions + : [ + { + group: 'default', + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + ], tags: [], muteAll: false, enabled: false, @@ -229,6 +232,7 @@ describe('action_form', () => { setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) } + setHasActionsWithBrokenConnector={deps!.setHasActionsWithBrokenConnector} http={deps!.http} actionTypeRegistry={deps!.actionTypeRegistry} defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'} @@ -306,6 +310,7 @@ describe('action_form', () => { .find(`EuiToolTip [data-test-subj="${actionType.id}-ActionTypeSelectOption"]`) .exists() ).toBeFalsy(); + expect(deps.setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(false); }); it('does not render action types disabled by config', async () => { @@ -392,5 +397,27 @@ describe('action_form', () => { ); expect(actionOption.exists()).toBeFalsy(); }); + + it('recognizes actions with broken connectors', async () => { + await setup([ + { + group: 'default', + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + { + group: 'default', + id: 'connector-doesnt-exist', + actionTypeId: actionType.id, + params: { + message: 'broken', + }, + }, + ]); + expect(deps.setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(true); + }); }); }); 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 1b176e0f63dbd..94571c4eb1e5b 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 @@ -62,6 +62,7 @@ interface ActionAccordionFormProps { messageVariables?: ActionVariable[]; defaultActionMessage?: string; setHasActionsDisabled?: (value: boolean) => void; + setHasActionsWithBrokenConnector?: (value: boolean) => void; capabilities: ApplicationStart['capabilities']; } @@ -83,6 +84,7 @@ export const ActionForm = ({ defaultActionMessage, toastNotifications, setHasActionsDisabled, + setHasActionsWithBrokenConnector, capabilities, docLinks, }: ActionAccordionFormProps) => { @@ -171,6 +173,16 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectors, actionTypesIndex]); + useEffect(() => { + const hasActionWithBrokenConnector = actions.some( + (action) => !connectors.find((connector) => connector.id === action.id) + ); + if (setHasActionsWithBrokenConnector) { + setHasActionsWithBrokenConnector(hasActionWithBrokenConnector); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actions, connectors]); + const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 999873a650f07..b60aa04ee9f27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -38,6 +38,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); + const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState( + false + ); const setAlert = (key: string, value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); }; @@ -155,6 +158,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { errors={errors} canChangeTrigger={false} setHasActionsDisabled={setHasActionsDisabled} + setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', { defaultMessage: 'edit', })" @@ -176,7 +180,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { data-test-subj="saveEditedAlertButton" type="submit" iconType="check" - isDisabled={hasErrors || hasActionErrors} + isDisabled={hasErrors || hasActionErrors || hasActionsWithBrokenConnector} isLoading={isSaving} onClick={async () => { setIsSaving(true); 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 c69c33c0fe22e..8800f149c033b 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 @@ -81,6 +81,7 @@ interface AlertFormProps { errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button setHasActionsDisabled?: (value: boolean) => void; + setHasActionsWithBrokenConnector?: (value: boolean) => void; operation: string; } @@ -90,6 +91,7 @@ export const AlertForm = ({ dispatch, errors, setHasActionsDisabled, + setHasActionsWithBrokenConnector, operation, }: AlertFormProps) => { const alertsContext = useAlertsContext(); @@ -260,6 +262,7 @@ export const AlertForm = ({ From d859ad3683d4f0c6c454d7cc2f2e99f3ba852262 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 16 Oct 2020 07:43:03 -0400 Subject: [PATCH 43/79] [Ingest Manager] add skipIfNoDockerRegistry to package_install_complete test (#80779) * fix missing skipIfNoDockerRegistry * skip afterEach if server doesn't exist --- .../apis/epm/data_stream.ts | 3 ++ .../apis/epm/package_install_complete.ts | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts index b9558240ca007..d1d909f773a2b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -12,6 +12,8 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); const pkgName = 'datastreams'; const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; @@ -63,6 +65,7 @@ export default function (providerContext: FtrProviderContext) { }); }); afterEach(async () => { + if (!server) return; await es.transport.request({ method: 'DELETE', path: `/_data_stream/${logsTemplateName}-default`, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts index 6b43c9d74c6bf..6fd4b64f0ee5e 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts @@ -9,6 +9,7 @@ import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL, } from '../../../../plugins/ingest_manager/common'; +import { skipIfNoDockerRegistry } from '../../helpers'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; export default function (providerContext: FtrProviderContext) { @@ -19,12 +20,14 @@ export default function (providerContext: FtrProviderContext) { const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; describe('setup checks packages completed install', async () => { + skipIfNoDockerRegistry(providerContext); describe('package install', async () => { before(async () => { await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }); it('should have not reinstalled if package install completed', async function () { const packageBeforeSetup = await kibanaServer.savedObjects.get({ @@ -32,7 +35,7 @@ export default function (providerContext: FtrProviderContext) { id: pkgName, }); const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -51,7 +54,7 @@ export default function (providerContext: FtrProviderContext) { install_started_at: previousInstallDate, }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -71,7 +74,7 @@ export default function (providerContext: FtrProviderContext) { install_started_at: previousInstallDate, }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -83,7 +86,8 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .expect(200); }); }); describe('package update', async () => { @@ -91,11 +95,13 @@ export default function (providerContext: FtrProviderContext) { await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.2.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }); it('should have not reinstalled if package update completed', async function () { const packageBeforeSetup = await kibanaServer.savedObjects.get({ @@ -103,7 +109,7 @@ export default function (providerContext: FtrProviderContext) { id: pkgName, }); const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -124,7 +130,7 @@ export default function (providerContext: FtrProviderContext) { install_version: pkgUpdateVersion, // set version back }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -147,7 +153,7 @@ export default function (providerContext: FtrProviderContext) { version: pkgVersion, // set version back }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -160,7 +166,8 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .expect(200); }); }); }); From 149b63c9bd425878c9d66fd171d34e3b67b1dba3 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 16 Oct 2020 14:58:07 +0300 Subject: [PATCH 44/79] [Timelion] Remove kui usage (#80287) * [Timelion] Remove kui usage * Fix custom checkbox * Add tim prefix to the new classes * Fix functional test * PR fixes * Fix type * PR comments * Remove the last fontawesome usages * fix timelion links Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/timelion/public/_app.scss | 62 +++++++++++++++++++ .../timelion/public/directives/_form.scss | 48 ++++++++++++++ .../directives/_saved_object_finder.scss | 37 +++++++++++ .../public/directives/cells/_cells.scss | 1 - .../public/directives/cells/cells.html | 6 +- .../directives/fullscreen/fullscreen.html | 2 +- .../directives/saved_object_finder.html | 32 ++++------ .../public/directives/saved_object_finder.js | 1 - .../saved_object_save_as_checkbox.html | 9 +-- .../directives/timelion_expression_input.html | 2 +- .../timelion_help/_timelion_help.scss | 8 +++ .../timelion_help/timelion_help.html | 30 ++++----- .../timelion_interval/_timelion_interval.scss | 6 ++ .../timelion_interval/timelion_interval.html | 2 +- src/plugins/timelion/public/index.html | 15 +++-- .../timelion/public/partials/load_sheet.html | 2 +- .../timelion/public/partials/save_sheet.html | 11 ++-- .../public/partials/sheet_options.html | 2 +- src/plugins/timelion/public/plugin.ts | 8 +-- 19 files changed, 217 insertions(+), 67 deletions(-) diff --git a/src/plugins/timelion/public/_app.scss b/src/plugins/timelion/public/_app.scss index 8b9078caba5a8..d8a6eb423a670 100644 --- a/src/plugins/timelion/public/_app.scss +++ b/src/plugins/timelion/public/_app.scss @@ -15,7 +15,69 @@ margin: $euiSizeM; } +.timApp__title { + display: flex; + align-items: center; + padding: $euiSizeM $euiSizeS; + font-size: $euiSize; + font-weight: $euiFontWeightBold; + border-bottom: 1px solid $euiColorLightShade; + flex-grow: 1; + background-color: $euiColorEmptyShade; +} + .timApp__stats { font-weight: $euiFontWeightRegular; color: $euiColorMediumShade; } + +.timApp__form { + display: flex; + align-items: flex-start; + margin-top: $euiSize; + margin-bottom: $euiSize; +} + +.timApp__expression { + display: flex; + flex: 1; + margin-right: $euiSizeS; +} + +.timApp__button { + margin-top: $euiSizeS; + padding: $euiSizeXS $euiSizeM; + font-size: $euiSize; + border: none; + border-radius: $euiSizeXS; + color: $euiColorEmptyShade; + background-color: $euiColorPrimary; +} + +.timApp__button--secondary { + margin-top: $euiSizeS; + padding: $euiSizeXS $euiSizeM; + font-size: $euiSize; + border: 1px solid $euiColorPrimary; + border-radius: $euiSizeXS; + color: $euiColorPrimary; + width: 100%; +} + +.timApp__sectionTitle { + margin-bottom: $euiSizeM; + font-size: 18px; + color: $euiColorDarkestShade; +} + +.timApp__helpText { + margin-bottom: $euiSize; + font-size: 14px; + color: $euiColorDarkShade; +} + +.timApp__label { + font-size: $euiSize; + line-height: 1.5; + font-weight: $euiFontWeightBold; +} diff --git a/src/plugins/timelion/public/directives/_form.scss b/src/plugins/timelion/public/directives/_form.scss index 3fcf70700a864..370dd25f8263f 100644 --- a/src/plugins/timelion/public/directives/_form.scss +++ b/src/plugins/timelion/public/directives/_form.scss @@ -34,3 +34,51 @@ select.form-control { .fullWidth { width: 100%; } + +.timDropdownWarning { + margin-bottom: $euiSize; + padding: $euiSizeXS $euiSizeS; + color: $euiColorDarkestShade; + border-left: solid 2px $euiColorDanger; + font-size: $euiSizeM; +} + +.timFormCheckbox { + display: flex; + align-items: center; + line-height: 1.5; + position: relative; +} + +.timFormCheckbox__input { + appearance: none; + background-color: $euiColorLightestShade; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + width: $euiSize; + height: $euiSize; + font-size: $euiSizeM; + transition: background-color .1s linear; +} + +.timFormCheckbox__input:checked { + border-color: $euiColorPrimary; + background-color: $euiColorPrimary; +} + +.timFormCheckbox__icon { + position: absolute; + top: 0; + left: 2px; +} + +.timFormTextarea { + padding: $euiSizeXS $euiSizeM; + font-size: $euiSize; + line-height: 1.5; + color: $euiColorDarkestShade; + background-color: $euiFormBackgroundColor; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + transition: border-color .1s linear; +} diff --git a/src/plugins/timelion/public/directives/_saved_object_finder.scss b/src/plugins/timelion/public/directives/_saved_object_finder.scss index e1a055a5f49e9..3a2489afb5721 100644 --- a/src/plugins/timelion/public/directives/_saved_object_finder.scss +++ b/src/plugins/timelion/public/directives/_saved_object_finder.scss @@ -27,6 +27,42 @@ saved-object-finder { + .timSearchBar { + display: flex; + align-items: center; + } + + .timSearchBar__section { + position: relative; + margin-right: $euiSize; + flex: 1; + } + + .timSearchBar__icon { + position: absolute; + top: $euiSizeS; + left: $euiSizeS; + font-size: $euiSize; + color: $euiColorDarkShade; + } + + .timSearchBar__input { + padding: $euiSizeS $euiSizeM; + color: $euiColorDarkestShade; + background-color: $euiColorEmptyShade; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + transition: border-color .1s linear; + padding-left: $euiSizeXL; + width: 100%; + font-size: $euiSize; + } + + .timSearchBar__pagecount { + font-size: $euiSize; + color: $euiColorDarkShade; + } + .list-sort-button { border-top-left-radius: 0; border-top-right-radius: 0; @@ -34,6 +70,7 @@ saved-object-finder { padding: $euiSizeS $euiSize; font-weight: $euiFontWeightRegular; background-color: $euiColorLightestShade; + margin-top: $euiSize; } .li-striped { diff --git a/src/plugins/timelion/public/directives/cells/_cells.scss b/src/plugins/timelion/public/directives/cells/_cells.scss index 899bf984e72c8..6cd71378a81de 100644 --- a/src/plugins/timelion/public/directives/cells/_cells.scss +++ b/src/plugins/timelion/public/directives/cells/_cells.scss @@ -33,7 +33,6 @@ text-align: center; width: $euiSizeL; height: $euiSizeL; - line-height: $euiSizeL; border-radius: $euiSizeL / 2; border: $euiBorderThin; background-color: $euiColorLightestShade; diff --git a/src/plugins/timelion/public/directives/cells/cells.html b/src/plugins/timelion/public/directives/cells/cells.html index 6be1b089d2deb..f90b85abaf920 100644 --- a/src/plugins/timelion/public/directives/cells/cells.html +++ b/src/plugins/timelion/public/directives/cells/cells.html @@ -25,7 +25,7 @@ tooltip-append-to-body="1" aria-label="{{ ::'timelion.cells.actions.removeAriaLabel' | i18n: { defaultMessage: 'Remove chart' } }}" > - +
diff --git a/src/plugins/timelion/public/directives/fullscreen/fullscreen.html b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html index 194596ba79d0e..1ed6aa82ea3b9 100644 --- a/src/plugins/timelion/public/directives/fullscreen/fullscreen.html +++ b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html @@ -8,7 +8,7 @@ tooltip-append-to-body="1" aria-label="{{ ::'timelion.fullscreen.exitAriaLabel' | i18n: { defaultMessage: 'Exit full screen' } }}" > - +
diff --git a/src/plugins/timelion/public/directives/saved_object_finder.html b/src/plugins/timelion/public/directives/saved_object_finder.html index ad148801c03a4..1ce10efe4e0a8 100644 --- a/src/plugins/timelion/public/directives/saved_object_finder.html +++ b/src/plugins/timelion/public/directives/saved_object_finder.html @@ -1,13 +1,11 @@
-
-
-
- +
+
+ -
-
-

+

-
+
@@ -45,7 +45,7 @@ @@ -82,7 +82,7 @@ @@ -100,7 +100,7 @@ @@ -222,7 +222,7 @@ @@ -230,7 +230,7 @@ @@ -371,7 +371,7 @@ @@ -379,7 +379,7 @@ @@ -484,7 +484,7 @@ @@ -492,7 +492,7 @@ @@ -587,7 +587,7 @@ @@ -596,7 +596,7 @@ @@ -606,7 +606,7 @@

@@ -618,7 +618,7 @@
-
+
. diff --git a/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss b/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss index b371c4400a303..7ce09155cafd8 100644 --- a/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss +++ b/src/plugins/timelion/public/directives/timelion_interval/_timelion_interval.scss @@ -4,6 +4,12 @@ timelion-interval { .timInterval__input { width: $euiSizeXL * 2; + padding: $euiSizeXS $euiSizeM; + color: $euiColorDarkestShade; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + transition: border-color .1s linear; + font-size: 14px; } .timInterval__input--compact { diff --git a/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html index 11c79e6a16820..49009355e49f4 100644 --- a/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html +++ b/src/plugins/timelion/public/directives/timelion_interval/timelion_interval.html @@ -1,7 +1,7 @@ - + -
+
-
+
@@ -62,14 +61,14 @@
-
+

diff --git a/src/plugins/timelion/public/partials/save_sheet.html b/src/plugins/timelion/public/partials/save_sheet.html index a0e0727f3ec82..7773a9d25df71 100644 --- a/src/plugins/timelion/public/partials/save_sheet.html +++ b/src/plugins/timelion/public/partials/save_sheet.html @@ -19,7 +19,7 @@
@@ -28,20 +28,21 @@ id="savedSheet" ng-model="opts.savedSheet.title" input-focus="select" - class="form-control kuiVerticalRhythmSmall" + class="form-control" + style="margin-bottom: 4px;" placeholder="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputPlaceholder' | i18n: { defaultMessage: 'Name this sheet...' } }}" aria-label="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputAriaLabel' | i18n: { defaultMessage: 'Name' } }}" > diff --git a/src/plugins/timelion/public/partials/sheet_options.html b/src/plugins/timelion/public/partials/sheet_options.html index e882cfe52958e..eae5709331659 100644 --- a/src/plugins/timelion/public/partials/sheet_options.html +++ b/src/plugins/timelion/public/partials/sheet_options.html @@ -1,6 +1,6 @@

diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index b435cc6fd399b..e5bfe7a27ad10 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -21,7 +21,6 @@ import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { CoreSetup, - CoreStart, Plugin, PluginInitializerContext, DEFAULT_APP_CATEGORIES, @@ -31,7 +30,7 @@ import { AppNavLinkStatus, } from '../../../core/public'; import { Panel } from './panels/panel'; -import { initAngularBootstrap, KibanaLegacyStart } from '../../kibana_legacy/public'; +import { initAngularBootstrap } from '../../kibana_legacy/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; @@ -53,7 +52,6 @@ export interface TimelionPluginStartDependencies { visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; savedObjects: SavedObjectsStart; - kibanaLegacy: KibanaLegacyStart; } /** @internal */ @@ -142,9 +140,7 @@ export class TimelionPlugin }); } - public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) { - kibanaLegacy.loadFontAwesome(); - } + public start() {} public stop(): void { if (this.stopUrlTracking) { From 177f4345636077c8d5bf1dc52a6774cecd5b025e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 16 Oct 2020 14:00:19 +0100 Subject: [PATCH 45/79] skip flaky suite (#79463) --- test/functional/apps/dashboard/url_field_formatter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/dashboard/url_field_formatter.ts b/test/functional/apps/dashboard/url_field_formatter.ts index 9b05b9b777b94..a18ad740681bf 100644 --- a/test/functional/apps/dashboard/url_field_formatter.ts +++ b/test/functional/apps/dashboard/url_field_formatter.ts @@ -46,7 +46,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(currentUrl).to.equal(fieldUrl); }; - describe('Changing field formatter to Url', () => { + // FLAKY: https://github.com/elastic/kibana/issues/79463 + describe.skip('Changing field formatter to Url', () => { before(async function () { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ From 8e51f0fb176f7f0126407f08a57902b1304f5c57 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 16 Oct 2020 15:33:33 +0200 Subject: [PATCH 46/79] Fix codeowners (#80826) --- .github/CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8f2c27ac7c3cf..5a9e8bc585119 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -119,18 +119,18 @@ #CC# /x-pack/plugins/beats_management/ @elastic/beats # Canvas -/src/plugins/dashboard/ @elastic/kibana-app -/src/plugins/input_control_vis/ @elastic/kibana-app -/src/plugins/vis_type_markdown/ @elastic/kibana-app +/src/plugins/dashboard/ @elastic/kibana-canvas +/src/plugins/input_control_vis/ @elastic/kibana-canvas +/src/plugins/vis_type_markdown/ @elastic/kibana-canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas -/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app +/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-canvas /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas -#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-canvas +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-canvas #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-canvas #CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app -#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app +#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-canvas +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-canvas # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon From 322094a1e8c6ae592ce06031e159fc941f33963a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 16 Oct 2020 15:52:35 +0200 Subject: [PATCH 47/79] [ILM] Add esErrorHandler for the new es js client (#80302) * [ILM] Add esErrorHandler for the new es js client * [ILM] Fix function call params * Rename function * Rename function * Rename function * [ILM] Change import of handleEsError to lib dependency * [ILM] Add comments about legacy and new es js client * [ILM] Add type casting for ResponseError Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../errors/handle_es_error.ts | 43 +++++++++++++++++++ .../errors/index.ts | 1 + .../errors/is_es_error.ts | 4 ++ .../es_ui_shared/server/errors/index.ts | 2 +- src/plugins/es_ui_shared/server/index.ts | 2 +- .../server/plugin.ts | 4 ++ .../api/index/register_add_policy_route.ts | 17 +++----- .../routes/api/index/register_remove_route.ts | 17 +++----- .../routes/api/index/register_retry_route.ts | 13 ++---- .../api/nodes/register_details_route.ts | 17 +++----- .../routes/api/nodes/register_list_route.ts | 18 ++++---- .../api/policies/register_create_route.ts | 17 +++----- .../api/policies/register_delete_route.ts | 17 +++----- .../api/policies/register_fetch_route.ts | 13 ++---- .../snapshot_policies/register_fetch_route.ts | 13 ++---- .../templates/register_add_policy_route.ts | 17 +++----- .../api/templates/register_fetch_route.ts | 13 ++---- .../server/shared_imports.ts | 7 +++ .../server/types.ts | 4 ++ 19 files changed, 127 insertions(+), 112 deletions(-) create mode 100644 src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/shared_imports.ts diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts new file mode 100644 index 0000000000000..1c3643898dd1d --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ApiError } from '@elastic/elasticsearch'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { IKibanaResponse, KibanaResponseFactory } from 'kibana/server'; + +interface EsErrorHandlerParams { + error: ApiError; + response: KibanaResponseFactory; +} + +/* + * For errors returned by the new elasticsearch js client. + */ +export const handleEsError = ({ error, response }: EsErrorHandlerParams): IKibanaResponse => { + // error.name is slightly better in terms of performance, since all errors now have name property + if (error.name === 'ResponseError') { + const { statusCode, body } = error as ResponseError; + return response.customError({ + statusCode, + body: { message: body.error?.reason }, + }); + } + // Case: default + return response.internalError({ body: error }); +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts index 0d025442f4a92..484dc17868ab0 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/index.ts @@ -18,3 +18,4 @@ */ export { isEsError } from './is_es_error'; +export { handleEsError } from './handle_es_error'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts index 1e212307ca1cc..80a53aac328a4 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/is_es_error.ts @@ -25,6 +25,10 @@ interface RequestError extends Error { statusCode?: number; } +/* + * @deprecated + * Only works with legacy elasticsearch js client errors and will be removed after 7.x last + */ export function isEsError(err: RequestError) { const isInstanceOfEsError = err instanceof esErrorsParent; const hasStatusCode = Boolean(err.statusCode); diff --git a/src/plugins/es_ui_shared/server/errors/index.ts b/src/plugins/es_ui_shared/server/errors/index.ts index c18374cd9ec31..532e02774ff50 100644 --- a/src/plugins/es_ui_shared/server/errors/index.ts +++ b/src/plugins/es_ui_shared/server/errors/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { isEsError } from '../../__packages_do_not_import__/errors'; +export { isEsError, handleEsError } from '../../__packages_do_not_import__/errors'; diff --git a/src/plugins/es_ui_shared/server/index.ts b/src/plugins/es_ui_shared/server/index.ts index 0118bbda53262..b2c9c85d956ba 100644 --- a/src/plugins/es_ui_shared/server/index.ts +++ b/src/plugins/es_ui_shared/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { isEsError } from './errors'; +export { isEsError, handleEsError } from './errors'; /** dummy plugin*/ export function plugin() { diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 40037d0c1e777..e87f4aa17c0ef 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -14,6 +14,7 @@ import { PluginInitializerContext, LegacyAPICaller, } from 'src/core/server'; +import { handleEsError } from './shared_imports'; import { Index as IndexWithoutIlm } from '../../index_management/common/types'; import { PLUGIN } from '../common/constants'; @@ -99,6 +100,9 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { @@ -47,15 +51,8 @@ export function registerAddPolicyRoute({ router, license }: RouteDependencies) { alias ); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts index a83a3fa1378c8..15c3e7b866c79 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_remove_route.ts @@ -26,7 +26,11 @@ const bodySchema = schema.object({ indexNames: schema.arrayOf(schema.string()), }); -export function registerRemoveRoute({ router, license }: RouteDependencies) { +export function registerRemoveRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/index/remove'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -36,15 +40,8 @@ export function registerRemoveRoute({ router, license }: RouteDependencies) { try { await removeLifecycle(context.core.elasticsearch.client.asCurrentUser, indexNames); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts index cdcf5ed4b7ac4..28bced0fb5a8f 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/index/register_retry_route.ts @@ -27,7 +27,7 @@ const bodySchema = schema.object({ indexNames: schema.arrayOf(schema.string()), }); -export function registerRetryRoute({ router, license }: RouteDependencies) { +export function registerRetryRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.post( { path: addBasePath('/index/retry'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -37,15 +37,8 @@ export function registerRetryRoute({ router, license }: RouteDependencies) { try { await retryLifecycle(context.core.elasticsearch.client.asCurrentUser, indexNames); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts index 57034af324ed5..41b93ba59e3f5 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_details_route.ts @@ -29,7 +29,11 @@ const paramsSchema = schema.object({ nodeAttrs: schema.string(), }); -export function registerDetailsRoute({ router, license }: RouteDependencies) { +export function registerDetailsRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.get( { path: addBasePath('/nodes/{nodeAttrs}/details'), validate: { params: paramsSchema } }, license.guardApiRoute(async (context, request, response) => { @@ -40,15 +44,8 @@ export function registerDetailsRoute({ router, license }: RouteDependencies) { const statsResponse = await context.core.elasticsearch.client.asCurrentUser.nodes.stats(); const okResponse = { body: findMatchingNodes(statsResponse.body, nodeAttrs) }; return response.ok(okResponse); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts index bb1679e695e14..53955d93c1e09 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -62,7 +62,12 @@ export function convertSettingsIntoLists( ); } -export function registerListRoute({ router, config, license }: RouteDependencies) { +export function registerListRoute({ + router, + config, + license, + lib: { handleEsError }, +}: RouteDependencies) { const { filteredNodeAttributes } = config; const NODE_ATTRS_KEYS_TO_IGNORE: string[] = [ @@ -95,15 +100,8 @@ export function registerListRoute({ router, config, license }: RouteDependencies disallowedNodeAttributes ); return response.ok({ body }); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 359b275622f0c..d8e40e3b30410 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -134,7 +134,11 @@ const bodySchema = schema.object({ }), }); -export function registerCreateRoute({ router, license }: RouteDependencies) { +export function registerCreateRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/policies'), validate: { body: bodySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -144,15 +148,8 @@ export function registerCreateRoute({ router, license }: RouteDependencies) { try { await createPolicy(context.core.elasticsearch.client.asCurrentUser, name, phases); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts index cb394c12c46fa..b0363cb7c3bc6 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_delete_route.ts @@ -23,7 +23,11 @@ const paramsSchema = schema.object({ policyNames: schema.string(), }); -export function registerDeleteRoute({ router, license }: RouteDependencies) { +export function registerDeleteRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.delete( { path: addBasePath('/policies/{policyNames}'), validate: { params: paramsSchema } }, license.guardApiRoute(async (context, request, response) => { @@ -33,15 +37,8 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { try { await deletePolicies(context.core.elasticsearch.client.asCurrentUser, policyNames); return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts index 8cbea25666378..fc5f369e588f1 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts @@ -57,7 +57,7 @@ const querySchema = schema.object({ withIndices: schema.boolean({ defaultValue: false }), }); -export function registerFetchRoute({ router, license }: RouteDependencies) { +export function registerFetchRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/policies'), validate: { query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -75,15 +75,8 @@ export function registerFetchRoute({ router, license }: RouteDependencies) { await addLinkedIndices(asCurrentUser, policiesMap); } return response.ok({ body: formatPolicies(policiesMap) }); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts index 869be3d557040..00afc31c03bad 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts @@ -14,7 +14,7 @@ async function fetchSnapshotPolicies(client: ElasticsearchClient): Promise return response.body; } -export function registerFetchRoute({ router, license }: RouteDependencies) { +export function registerFetchRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/snapshot_policies'), validate: false }, license.guardApiRoute(async (context, request, response) => { @@ -23,15 +23,8 @@ export function registerFetchRoute({ router, license }: RouteDependencies) { context.core.elasticsearch.client.asCurrentUser ); return response.ok({ body: Object.keys(policiesByName) }); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts index 7e7f3f1f725f8..667491ef4af65 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts @@ -92,7 +92,11 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerAddPolicyRoute({ router, license }: RouteDependencies) { +export function registerAddPolicyRoute({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { router.post( { path: addBasePath('/template'), validate: { body: bodySchema, query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -118,15 +122,8 @@ export function registerAddPolicyRoute({ router, license }: RouteDependencies) { }); } return response.ok(); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts index fbd102d3be1eb..35860d2177329 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts @@ -80,7 +80,7 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerFetchRoute({ router, license }: RouteDependencies) { +export function registerFetchRoute({ router, license, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/templates'), validate: { query: querySchema } }, license.guardApiRoute(async (context, request, response) => { @@ -92,15 +92,8 @@ export function registerFetchRoute({ router, license }: RouteDependencies) { ); const okResponse = { body: filterTemplates(templates, isLegacy) }; return response.ok(okResponse); - } catch (e) { - if (e.name === 'ResponseError') { - return response.customError({ - statusCode: e.statusCode, - body: { message: e.body.error?.reason }, - }); - } - // Case: default - return response.internalError({ body: e }); + } catch (error) { + return handleEsError({ error, response }); } }) ); diff --git a/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts new file mode 100644 index 0000000000000..068cddcee4c86 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/index_lifecycle_management/server/types.ts b/x-pack/plugins/index_lifecycle_management/server/types.ts index e34dc8e4b1a52..8de7a01f1febc 100644 --- a/x-pack/plugins/index_lifecycle_management/server/types.ts +++ b/x-pack/plugins/index_lifecycle_management/server/types.ts @@ -11,6 +11,7 @@ import { LicensingPluginSetup } from '../../licensing/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { License } from './services'; import { IndexLifecycleManagementConfig } from './config'; +import { handleEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; @@ -22,4 +23,7 @@ export interface RouteDependencies { router: IRouter; config: IndexLifecycleManagementConfig; license: License; + lib: { + handleEsError: typeof handleEsError; + }; } From b06a04dac49929f1c54abddeb9620fa9a85d58ce Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 16 Oct 2020 16:53:05 +0300 Subject: [PATCH 48/79] [Visualizations] Fix bad color mapping with multiple split series (#80801) * [Vislib] Fix bad color mapping with multiple split series * Add unit test to cover the new colors generation --- src/plugins/charts/public/services/colors/color_palette.ts | 2 +- .../charts/public/services/colors/colors_palette.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/charts/public/services/colors/color_palette.ts b/src/plugins/charts/public/services/colors/color_palette.ts index e1c32fe68da12..df76edb1e30ed 100644 --- a/src/plugins/charts/public/services/colors/color_palette.ts +++ b/src/plugins/charts/public/services/colors/color_palette.ts @@ -58,7 +58,7 @@ export function createColorPalette(num: number): string[] { const seedLength = seedColors.length; _.times(num - seedLength, function (i) { - colors.push(hsl((fraction(i + seedLength + 1) * 360 + offset) % 360, 0.5, 0.5).hex()); + colors.push(hsl((fraction(i + seedLength + 1) * 360 + offset) % 360, 50, 50).hex()); }); return colors; diff --git a/src/plugins/charts/public/services/colors/colors_palette.test.ts b/src/plugins/charts/public/services/colors/colors_palette.test.ts index 02ff5a6056d54..273a36f6a43a6 100644 --- a/src/plugins/charts/public/services/colors/colors_palette.test.ts +++ b/src/plugins/charts/public/services/colors/colors_palette.test.ts @@ -90,4 +90,8 @@ describe('Color Palette', () => { it('should create new darker colors when input is greater than 72', () => { expect(createColorPalette(num3)[72]).not.toEqual(seedColors[0]); }); + + it('should create new colors and convert them correctly', () => { + expect(createColorPalette(num3)[72]).toEqual('#404ABF'); + }); }); From 949c5a55b2d49768c744476cd35d23bea70a7a77 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 16 Oct 2020 16:38:35 +0200 Subject: [PATCH 49/79] Fix navigateToApp logic when navigating to the current app. (#80809) --- .../application/application_service.tsx | 10 ++- .../application_service.test.tsx | 70 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 0d08f6f3007b0..4d54d4831698b 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -242,11 +242,17 @@ export class ApplicationService { appId, { path, state, replace = false }: NavigateToAppOptions = {} ) => { - if (await this.shouldNavigate(overlays)) { + const currentAppId = this.currentAppId$.value; + const navigatingToSameApp = currentAppId === appId; + const shouldNavigate = navigatingToSameApp ? true : await this.shouldNavigate(overlays); + + if (shouldNavigate) { if (path === undefined) { path = applications$.value.get(appId)?.defaultPath; } - this.appInternalStates.delete(this.currentAppId$.value!); + if (!navigatingToSameApp) { + this.appInternalStates.delete(this.currentAppId$.value!); + } this.navigate!(getAppUrl(availableMounters, appId, path), state, replace); this.currentAppId$.next(appId); } diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index d28486928b7e2..82933576bc493 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -258,6 +258,34 @@ describe('ApplicationService', () => { expect(history.entries.length).toEqual(2); expect(history.entries[1].pathname).toEqual('/app/app1'); }); + + it('does not trigger navigation check if navigating to the current app', async () => { + startDeps.overlays.openConfirm.mockResolvedValue(false); + + const { register } = service.setup(setupDeps); + + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: ({ onAppLeave }: AppMountParameters) => { + onAppLeave((actions) => actions.confirm('confirmation-message', 'confirmation-title')); + return () => undefined; + }, + }); + + const { navigateToApp, getComponent } = await service.start(startDeps); + + update = createRenderer(getComponent()); + + await act(async () => { + await navigate('/app/app1'); + await navigateToApp('app1', { path: '/internal-path' }); + }); + + expect(startDeps.overlays.openConfirm).not.toHaveBeenCalled(); + expect(history.entries.length).toEqual(3); + expect(history.entries[2].pathname).toEqual('/app/app1/internal-path'); + }); }); describe('registering action menus', () => { @@ -331,6 +359,48 @@ describe('ApplicationService', () => { expect(await getValue(currentActionMenu$)).toBe(mounter2); }); + it('does not update the observable value when navigating to the current app', async () => { + const { register } = service.setup(setupDeps); + + let initialMount = true; + register(Symbol(), { + id: 'app1', + title: 'App1', + mount: async ({ setHeaderActionMenu }: AppMountParameters) => { + if (initialMount) { + setHeaderActionMenu(mounter1); + initialMount = false; + } + return () => undefined; + }, + }); + + const { navigateToApp, getComponent, currentActionMenu$ } = await service.start(startDeps); + update = createRenderer(getComponent()); + + let mountedMenuCount = 0; + currentActionMenu$.subscribe(() => { + mountedMenuCount++; + }); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + await act(async () => { + await navigateToApp('app1'); + await flushPromises(); + }); + + expect(await getValue(currentActionMenu$)).toBe(mounter1); + + // there is an initial 'undefined' emission + expect(mountedMenuCount).toBe(2); + }); + it('updates the observable value to undefined when switching to an application without action menu', async () => { const { register } = service.setup(setupDeps); From 52b35f350429e14b012566bc14e642a8fae9c8f5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 16 Oct 2020 16:49:06 +0200 Subject: [PATCH 50/79] [Lens] Add median operation (#79453) --- .../dimension_panel/dimension_editor.tsx | 6 +++++- .../dimension_panel/dimension_panel.test.tsx | 2 ++ .../operations/definitions/index.ts | 4 ++++ .../operations/definitions/metrics.tsx | 13 +++++++++++++ .../operations/operations.test.ts | 9 +++++++-- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 8b0c9011f2c27..ed0591219b559 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -264,7 +264,11 @@ export function DimensionEditor(props: DimensionEditorProps) { 3 ? 'lnsIndexPatternDimensionEditor__columns' : ''} gutterSize="none" - listItems={sideNavItems} + listItems={ + // add a padding item containing a non breakable space if the number of operations is not even + // otherwise the column layout will break within an element + sideNavItems.length % 2 === 1 ? [...sideNavItems, { label: '\u00a0' }] : sideNavItems + } maxWidth={false} />
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index d15825718682c..1bf5039ef05fa 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -993,9 +993,11 @@ describe('IndexPatternDimensionEditorPanel', () => { 'Average', 'Count', 'Maximum', + 'Median', 'Minimum', 'Sum', 'Unique count', + '\u00a0', ]); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 38aec866ca5cb..735015492bd5a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -18,6 +18,8 @@ import { SumIndexPatternColumn, maxOperation, MaxIndexPatternColumn, + medianOperation, + MedianIndexPatternColumn, } from './metrics'; import { dateHistogramOperation, DateHistogramIndexPatternColumn } from './date_histogram'; import { countOperation, CountIndexPatternColumn } from './count'; @@ -43,6 +45,7 @@ export type IndexPatternColumn = | AvgIndexPatternColumn | CardinalityIndexPatternColumn | SumIndexPatternColumn + | MedianIndexPatternColumn | CountIndexPatternColumn; export type FieldBasedIndexPatternColumn = Extract; @@ -59,6 +62,7 @@ const internalOperationDefinitions = [ averageOperation, cardinalityOperation, sumOperation, + medianOperation, countOperation, rangeOperation, ]; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx index c02f7bcb7d2cd..1d3ecc165ce74 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx @@ -87,6 +87,7 @@ export type SumIndexPatternColumn = MetricColumn<'sum'>; export type AvgIndexPatternColumn = MetricColumn<'avg'>; export type MinIndexPatternColumn = MetricColumn<'min'>; export type MaxIndexPatternColumn = MetricColumn<'max'>; +export type MedianIndexPatternColumn = MetricColumn<'median'>; export const minOperation = buildMetricOperation({ type: 'min', @@ -137,3 +138,15 @@ export const sumOperation = buildMetricOperation({ values: { name }, }), }); + +export const medianOperation = buildMetricOperation({ + type: 'median', + displayName: i18n.translate('xpack.lens.indexPattern.median', { + defaultMessage: 'Median', + }), + ofName: (name) => + i18n.translate('xpack.lens.indexPattern.medianOf', { + defaultMessage: 'Median of {name}', + values: { name }, + }), +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index c1bd4b84099b7..6808bc724f26b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -315,12 +315,12 @@ describe('getOperationTypesForField', () => { }, Object { "field": "bytes", - "operationType": "min", + "operationType": "max", "type": "field", }, Object { "field": "bytes", - "operationType": "max", + "operationType": "min", "type": "field", }, Object { @@ -338,6 +338,11 @@ describe('getOperationTypesForField', () => { "operationType": "cardinality", "type": "field", }, + Object { + "field": "bytes", + "operationType": "median", + "type": "field", + }, ], }, ] From 9157a62d3f8e0154c6239acb884037c7e03100e3 Mon Sep 17 00:00:00 2001 From: wolframhaussig <13997737+wolframhaussig@users.noreply.github.com> Date: Fri, 16 Oct 2020 16:59:49 +0200 Subject: [PATCH 51/79] Update known-plugins.asciidoc (#75388) Remove Conveyor as the Project has been archived by the owner (cherry picked from commit 8171283f3a2b836b535e1c681087370b9c8a9351) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/plugins/known-plugins.asciidoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index 8fc2b7381de83..7b24de42d8e1c 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -14,7 +14,6 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/sivasamyk/logtrail[LogTrail] - View, analyze, search and tail log events in realtime with a developer/sysadmin friendly interface * https://github.com/wtakase/kibana-own-home[Own Home] (wtakase) - enables multi-tenancy * https://github.com/asileon/kibana_shard_allocation[Shard Allocation] (asileon) - visualize elasticsearch shard allocation -* https://github.com/samtecspg/conveyor[Conveyor] - Simple (GUI) interface for importing data into Elasticsearch. * https://github.com/wazuh/wazuh-kibana-app[Wazuh] - Wazuh provides host-based security visibility using lightweight multi-platform agents. * https://github.com/TrumanDu/indices_view[Indices View] - View indices related information. * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API From ca8b03823bdce2bd722cace2d27c06058f6d11b9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 16 Oct 2020 18:07:11 +0300 Subject: [PATCH 52/79] [Security Solution][Cases] Fix bug with case connectors (#80642) * Fix bug with case connectors * Improve isCaseOwned function --- .../routes/api/__mocks__/request_responses.ts | 39 ++++ .../cases/configure/get_connectors.test.ts | 67 ++++++- .../api/cases/configure/get_connectors.ts | 44 +++-- .../basic/tests/configure/get_connectors.ts | 167 ++++++++++++++++-- .../case_api_integration/common/lib/utils.ts | 47 ++++- 5 files changed, 327 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index bd276bc91ca3e..ce35b99750419 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -62,6 +62,45 @@ export const getActions = (): FindActionResult[] => [ isPreconfigured: false, referencedByCount: 0, }, + { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: '789', + actionTypeId: '.resilient', + name: 'Connector without mapping', + config: { + apiUrl: 'https://elastic.resilient.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, ]; export const newConfiguration: CasesConfigureRequest = { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index d7a01ef069867..ee4dcc8e81b95 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -15,7 +15,6 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initCaseConfigureGetActionConnector } from './get_connectors'; -import { getActions } from '../../__mocks__/request_responses'; import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; describe('GET connectors', () => { @@ -24,7 +23,7 @@ describe('GET connectors', () => { routeHandler = await createRoute(initCaseConfigureGetActionConnector, 'get'); }); - it('returns the connectors', async () => { + it('returns case owned connectors', async () => { const req = httpServerMock.createKibanaRequest({ path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, method: 'get', @@ -38,9 +37,67 @@ describe('GET connectors', () => { const res = await routeHandler(context, req, kibanaResponseFactory); expect(res.status).toEqual(200); - expect(res.payload).toEqual( - getActions().filter((action) => action.actionTypeId === '.servicenow') - ); + expect(res.payload).toEqual([ + { + id: '123', + actionTypeId: '.servicenow', + name: 'ServiceNow', + config: { + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://dev102283.service-now.com', + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, + ]); }); it('it throws an error when actions client is null', async () => { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 545ccf82c3d78..c3e565a404e97 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -7,15 +7,44 @@ import Boom from 'boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FindActionResult } from '../../../../../../actions/server/types'; import { CASE_CONFIGURE_CONNECTORS_URL, - SUPPORTED_CONNECTORS, SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID, } from '../../../../../common/constants'; +/** + * We need to take into account connectors that have been created within cases and + * they do not have the isCaseOwned field. Checking for the existence of + * the mapping attribute ensures that the connector is indeed a case connector. + * Cases connector should always have a mapping. + */ + +interface CaseAction extends FindActionResult { + config?: { + isCaseOwned?: boolean; + incidentConfiguration?: Record; + }; +} + +const isCaseOwned = (action: CaseAction): boolean => { + if ( + [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ) + ) { + if (action.config?.isCaseOwned === true || action.config?.incidentConfiguration?.mapping) { + return true; + } + } + + return false; +}; + /* * Be aware that this api will only return 20 connectors */ @@ -34,18 +63,7 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter( - (action) => - SUPPORTED_CONNECTORS.includes(action.actionTypeId) && - // Need this filtering temporary to display only Case owned ServiceNow connectors - (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) || - ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) && - action.config?.isCaseOwned === true)) - ); + const results = (await actionsClient.getAll()).filter(isCaseOwned); return response.ok({ body: results }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts index 5ba1aac4c8f92..5195d28d84830 100644 --- a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -13,6 +13,8 @@ import { getServiceNowConnector, getJiraConnector, getResilientConnector, + getConnectorWithoutCaseOwned, + getConnectorWithoutMapping, } from '../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -36,13 +38,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return the correct connectors', async () => { - const { body: connectorOne } = await supertest + const { body: snConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send(getServiceNowConnector()) .expect(200); - const { body: connectorTwo } = await supertest + const { body: emailConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send({ @@ -59,22 +61,36 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - const { body: connectorThree } = await supertest + const { body: jiraConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send(getJiraConnector()) .expect(200); - const { body: connectorFour } = await supertest + const { body: resilientConnector } = await supertest .post('/api/actions/action') .set('kbn-xsrf', 'true') .send(getResilientConnector()) .expect(200); - actionsRemover.add('default', connectorOne.id, 'action', 'actions'); - actionsRemover.add('default', connectorTwo.id, 'action', 'actions'); - actionsRemover.add('default', connectorThree.id, 'action', 'actions'); - actionsRemover.add('default', connectorFour.id, 'action', 'actions'); + const { body: connectorWithoutCaseOwned } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getConnectorWithoutCaseOwned()) + .expect(200); + + const { body: connectorNoMapping } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send(getConnectorWithoutMapping()) + .expect(200); + + actionsRemover.add('default', snConnector.id, 'action', 'actions'); + actionsRemover.add('default', emailConnector.id, 'action', 'actions'); + actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); + actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); + actionsRemover.add('default', connectorWithoutCaseOwned.id, 'action', 'actions'); + actionsRemover.add('default', connectorNoMapping.id, 'action', 'actions'); const { body: connectors } = await supertest .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) @@ -82,16 +98,131 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); - expect(connectors.length).to.equal(3); - expect( - connectors.some((c: { actionTypeId: string }) => c.actionTypeId === '.servicenow') - ).to.equal(true); - expect(connectors.some((c: { actionTypeId: string }) => c.actionTypeId === '.jira')).to.equal( - true - ); - expect( - connectors.some((c: { actionTypeId: string }) => c.actionTypeId === '.resilient') - ).to.equal(true); + expect(connectors).to.eql([ + { + id: connectorWithoutCaseOwned.id, + actionTypeId: '.resilient', + name: 'Connector without isCaseOwned', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: null, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: jiraConnector.id, + actionTypeId: '.jira', + name: 'Jira Connector', + config: { + apiUrl: 'http://some.non.existent.com', + projectKey: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'summary', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: resilientConnector.id, + actionTypeId: '.resilient', + name: 'Resilient Connector', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + { + id: snConnector.id, + actionTypeId: '.servicenow', + name: 'ServiceNow Connector', + config: { + apiUrl: 'http://some.non.existent.com', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + isCaseOwned: true, + }, + isPreconfigured: false, + referencedByCount: 0, + }, + ]); }); }); }; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 8d28f647ce43b..262e14fac6d8c 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -116,7 +116,7 @@ export const getResilientConnector = () => ({ mapping: [ { source: 'title', - target: 'summary', + target: 'name', actionType: 'overwrite', }, { @@ -135,6 +135,51 @@ export const getResilientConnector = () => ({ }, }); +export const getConnectorWithoutCaseOwned = () => ({ + name: 'Connector without isCaseOwned', + actionTypeId: '.resilient', + secrets: { + apiKeyId: 'id', + apiKeySecret: 'secret', + }, + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + incidentConfiguration: { + mapping: [ + { + source: 'title', + target: 'name', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); + +export const getConnectorWithoutMapping = () => ({ + name: 'Connector without mapping', + actionTypeId: '.resilient', + secrets: { + apiKeyId: 'id', + apiKeySecret: 'secret', + }, + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + }, +}); + export const removeServerGeneratedPropertiesFromConfigure = ( config: Partial ): Partial => { From 4a46b9e5d4603fca14690e8485a203559a235c0b Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 16 Oct 2020 08:20:04 -0700 Subject: [PATCH 53/79] [keystore_cli] parse values as JSON before adding to keystore --- src/cli_keystore/add.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 232392f34c63b..d88256da1aa59 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -59,7 +59,15 @@ export async function add(keystore, key, options = {}) { value = await question(`Enter value for ${key}`, { mask: '*' }); } - keystore.add(key, value.trim()); + const parsedValue = value.trim(); + let parsedJsonValue; + try { + parsedJsonValue = JSON.parse(parsedValue); + } catch { + // noop, only treat value as json if it parses as JSON + } + + keystore.add(key, parsedJsonValue ?? parsedValue); keystore.save(); } From 98c751a0f57f1bf7a0562c60bfd6649bf4e00bad Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Fri, 16 Oct 2020 08:24:06 -0700 Subject: [PATCH 54/79] Moving loader to logo in header, add a slight 250ms pause (#78879) Puts a loader on the logo and replaces the bar that normally showed up. --- .../loading_indicator.test.tsx.snap | 20 +- .../header/__snapshots__/header.test.tsx.snap | 249 ++++++++++-------- .../public/chrome/ui/header/elastic_mark.tsx | 34 +++ src/core/public/chrome/ui/header/header.tsx | 2 +- .../public/chrome/ui/header/header_logo.scss | 4 + .../public/chrome/ui/header/header_logo.tsx | 21 +- .../chrome/ui/loading_indicator.test.tsx | 5 +- .../public/chrome/ui/loading_indicator.tsx | 38 ++- 8 files changed, 240 insertions(+), 133 deletions(-) create mode 100644 src/core/public/chrome/ui/header/elastic_mark.tsx create mode 100644 src/core/public/chrome/ui/header/header_logo.scss diff --git a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap index e6bf7e898d8c4..10e6e9befe4f9 100644 --- a/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap +++ b/src/core/public/chrome/ui/__snapshots__/loading_indicator.test.tsx.snap @@ -1,19 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`kbnLoadingIndicator is hidden by default 1`] = ` - `; exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = ` - `; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index e733c7fda5d5a..5e563c4061093 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1715,17 +1715,10 @@ exports[`Header renders 1`] = ` } } href="/" - navLinks$={ + loadingCount$={ BehaviorSubject { "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "", - "href": "", - "id": "kibana", - "title": "kibana", - }, - ], + "_value": 0, "closed": false, "hasError": false, "isStopped": false, @@ -1767,6 +1760,25 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "", + "href": "", + "id": "kibana", + "title": "kibana", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -1804,21 +1816,6 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[MockFunction]} - />, - , ], }, @@ -2821,17 +2818,10 @@ exports[`Header renders 1`] = ` } } href="/" - navLinks$={ + loadingCount$={ BehaviorSubject { "_isScalar": false, - "_value": Array [ - Object { - "baseUrl": "", - "href": "", - "id": "kibana", - "title": "kibana", - }, - ], + "_value": 0, "closed": false, "hasError": false, "isStopped": false, @@ -2873,6 +2863,25 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, + ], + "thrownError": null, + } + } + navLinks$={ + BehaviorSubject { + "_isScalar": false, + "_value": Array [ + Object { + "baseUrl": "", + "href": "", + "id": "kibana", + "title": "kibana", + }, + ], + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ Subscriber { "_parentOrParents": null, "_subscriptions": Array [ @@ -2910,66 +2919,6 @@ exports[`Header renders 1`] = ` "syncErrorThrown": false, "syncErrorValue": null, }, - ], - "thrownError": null, - } - } - navigateToApp={[MockFunction]} - > - - - - - - -
- - - + +
+ + + - - + className="chrHeaderLogo__mark" + > + + + Elastic + + + + + +
diff --git a/src/core/public/chrome/ui/header/elastic_mark.tsx b/src/core/public/chrome/ui/header/elastic_mark.tsx new file mode 100644 index 0000000000000..e4456e9b751f3 --- /dev/null +++ b/src/core/public/chrome/ui/header/elastic_mark.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { HTMLAttributes } from 'react'; + +export const ElasticMark = ({ ...props }: HTMLAttributes) => ( + + Elastic + + +); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index d0b39e362ecb7..7089ec1087271 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -112,8 +112,8 @@ export function Header({ forceNavigation$={observables.forceAppSwitcherNavigation$} navLinks$={observables.navLinks$} navigateToApp={application.navigateToApp} + loadingCount$={observables.loadingCount$} />, - , ], borders: 'none', }, diff --git a/src/core/public/chrome/ui/header/header_logo.scss b/src/core/public/chrome/ui/header/header_logo.scss new file mode 100644 index 0000000000000..f75fd9cfa2466 --- /dev/null +++ b/src/core/public/chrome/ui/header/header_logo.scss @@ -0,0 +1,4 @@ +.chrHeaderLogo__mark { + margin-left: $euiSizeS; + fill: $euiColorGhost; +} diff --git a/src/core/public/chrome/ui/header/header_logo.tsx b/src/core/public/chrome/ui/header/header_logo.tsx index 83e0c52ab3f3a..df961ebb0983f 100644 --- a/src/core/public/chrome/ui/header/header_logo.tsx +++ b/src/core/public/chrome/ui/header/header_logo.tsx @@ -17,13 +17,16 @@ * under the License. */ -import { EuiHeaderLogo } from '@elastic/eui'; +import './header_logo.scss'; import { i18n } from '@kbn/i18n'; import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; import Url from 'url'; import { ChromeNavLink } from '../..'; +import { ElasticMark } from './elastic_mark'; +import { HttpStart } from '../../../http'; +import { LoadingIndicator } from '../loading_indicator'; function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; @@ -90,23 +93,25 @@ interface Props { navLinks$: Observable; forceNavigation$: Observable; navigateToApp: (appId: string) => void; + loadingCount$?: ReturnType; } -export function HeaderLogo({ href, navigateToApp, ...observables }: Props) { +export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables }: Props) { const forceNavigation = useObservable(observables.forceNavigation$, false); const navLinks = useObservable(observables.navLinks$, []); return ( - onClick(e, forceNavigation, navLinks, navigateToApp)} + className="euiHeaderLogo" href={href} + data-test-subj="logo" aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', { - defaultMessage: 'Go to home page', + defaultMessage: 'Elastic home', })} > - Elastic - + + + ); } diff --git a/src/core/public/chrome/ui/loading_indicator.test.tsx b/src/core/public/chrome/ui/loading_indicator.test.tsx index ff56ca668ae02..2d45a3d079616 100644 --- a/src/core/public/chrome/ui/loading_indicator.test.tsx +++ b/src/core/public/chrome/ui/loading_indicator.test.tsx @@ -32,7 +32,10 @@ describe('kbnLoadingIndicator', () => { it('is visible when loadingCount is > 0', () => { const wrapper = shallow(); - expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator'); + // Pause the check beyond the 250ms delay that it has + setTimeout(() => { + expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator'); + }, 300); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/core/public/chrome/ui/loading_indicator.tsx b/src/core/public/chrome/ui/loading_indicator.tsx index ca3e95f722ec5..25ec52e8dbb58 100644 --- a/src/core/public/chrome/ui/loading_indicator.tsx +++ b/src/core/public/chrome/ui/loading_indicator.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { EuiLoadingSpinner, EuiProgress } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiProgress, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import classNames from 'classnames'; @@ -39,16 +39,26 @@ export class LoadingIndicator extends React.Component { - this.setState({ - visible: count > 0, - }); + if (this.increment > 1) { + clearTimeout(this.timer); + } + this.increment += this.increment; + this.timer = setTimeout(() => { + this.setState({ + visible: count > 0, + }); + }, 250); }); } componentWillUnmount() { if (this.loadingCountSubscription) { + clearTimeout(this.timer); this.loadingCountSubscription.unsubscribe(); this.loadingCountSubscription = undefined; } @@ -67,13 +77,27 @@ export class LoadingIndicator extends React.Component + ) : ( + + ); + + return !this.props.showAsBar ? ( + logo ) : ( Date: Fri, 16 Oct 2020 11:28:29 -0400 Subject: [PATCH 55/79] [Ingest]: ignore 404, check if there are transforms in results. (#80721) [Ingest]: ignore 404, check if there are transforms in results --- .../epm/elasticsearch/transform/remove.ts | 22 ++++++++++++------- .../elasticsearch/transform/transform.test.ts | 2 ++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts index 02d5dfc64d07d..5b5583a121e5d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants'; +import { appContextService } from '../../../app_context'; export const stopTransforms = async (transformIds: string[], callCluster: CallESAsCurrentUser) => { for (const transformId of transformIds) { @@ -28,7 +29,7 @@ export const deleteTransforms = async ( // get the index the transform const transformResponse: { count: number; - transforms: Array<{ + transforms?: Array<{ dest: { index: string; }; @@ -36,6 +37,7 @@ export const deleteTransforms = async ( } = await callCluster('transport.request', { method: 'GET', path: `/_transform/${transformId}`, + ignore: [404], }); await stopTransforms([transformId], callCluster); @@ -46,13 +48,17 @@ export const deleteTransforms = async ( ignore: [404], }); - // expect this to be 1 - for (const transform of transformResponse.transforms) { - await callCluster('transport.request', { - method: 'DELETE', - path: `/${transform?.dest?.index}`, - ignore: [404], - }); + if (transformResponse?.transforms) { + // expect this to be 1 + for (const transform of transformResponse.transforms) { + await callCluster('transport.request', { + method: 'DELETE', + path: `/${transform?.dest?.index}`, + ignore: [404], + }); + } + } else { + appContextService.getLogger().warn(`cannot find transform for ${transformId}`); } }) ); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts index 768c6af1d8915..2bf0ad12856f8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts @@ -160,6 +160,7 @@ describe('test transform install', () => { { method: 'GET', path: '/_transform/endpoint.metadata_current-default-0.15.0-dev.0', + ignore: [404], }, ], [ @@ -446,6 +447,7 @@ describe('test transform install', () => { { method: 'GET', path: '/_transform/endpoint.metadata-current-default-0.15.0-dev.0', + ignore: [404], }, ], [ From 849a50d0c23c3c975ba22c9d95e0e05f0f71f2ac Mon Sep 17 00:00:00 2001 From: Charlie Pichette <56399229+charlie-pichette@users.noreply.github.com> Date: Fri, 16 Oct 2020 09:37:18 -0600 Subject: [PATCH 56/79] Fix security solution template label (#80754) --- .github/ISSUE_TEMPLATE/security_solution_bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md index a7a12a6eb379d..bd7d57c72ea56 100644 --- a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -2,7 +2,7 @@ name: Bug report for Security Solution about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! title: '[Security Solution]' -labels: 'Team: Security Solution' +labels: Team: SecuritySolution --- **Describe the bug:** From 898e21f3470df8a67f3eab8e35fdb6fd94a1e4f4 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 16 Oct 2020 17:43:49 +0200 Subject: [PATCH 57/79] [Discover] Unskip flaky test (#80670) --- test/functional/apps/discover/_field_data.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index d9cb09432b26f..d45b8f4841cb6 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,21 +27,17 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/78689 - describe.skip('discover tab', function describeIndexTests() { + describe('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); - // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', }); - + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setDefaultAbsoluteRange(); }); - describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; From 7cdcf43ebaa9007eb03d32f2eef1157e8bffff2d Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Fri, 16 Oct 2020 10:58:19 -0500 Subject: [PATCH 58/79] [Workplace Search] Add unit tests for Groups components (#79894) * Add AddGroupModal test * Add ClearFiltersLink test * Add FilterableUsersList tests * Add FilterableUsersPopover tests * Add GroupManagerModal tests * Add GroupOverview tests * Add GroupRowSourcesDropdown test * Add GroupRowUsersDropdown tests * Add GroupRow tests * Add GroupSourcePrioritization tests * Add GroupSources tests * Add GroupSubNav tests * Add GroupUsersTable tests * Add GroupUsers tests * Add GroupsTable tests * Add ManageUsersModal test * Add SharedSourcesModal test * Add SourceOptionItem test * Add SourcesList tests * Add TableFilterSourcesDropdown test * Add TableFilterUsersDropdown test * Add TableFilters tests * Add UserOptionItem tests * Address PR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../workplace_search/__mocks__/groups.mock.ts | 23 ++++ .../components/add_group_modal.test.tsx | 65 ++++++++++ .../components/clear_filters_link.test.tsx | 40 ++++++ .../components/filterable_users_list.test.tsx | 121 ++++++++++++++++++ .../filterable_users_popover.test.tsx | 45 +++++++ .../components/group_manager_modal.test.tsx | 85 ++++++++++++ .../groups/components/group_manager_modal.tsx | 6 +- .../groups/components/group_overview.test.tsx | 112 ++++++++++++++++ .../groups/components/group_overview.tsx | 6 +- .../groups/components/group_row.test.tsx | 65 ++++++++++ .../views/groups/components/group_row.tsx | 4 +- .../group_row_sources_dropdown.test.tsx | 37 ++++++ .../group_row_users_dropdown.test.tsx | 66 ++++++++++ .../group_source_prioritization.test.tsx | 80 ++++++++++++ .../groups/components/group_sources.test.tsx | 47 +++++++ .../groups/components/group_sub_nav.test.tsx | 32 +++++ .../groups/components/group_users.test.tsx | 53 ++++++++ .../components/group_users_table.test.tsx | 62 +++++++++ .../groups/components/groups_table.test.tsx | 80 ++++++++++++ .../components/manage_users_modal.test.tsx | 45 +++++++ .../components/shared_sources_modal.test.tsx | 48 +++++++ .../components/source_option_item.test.tsx | 27 ++++ .../groups/components/sources_list.test.tsx | 48 +++++++ .../table_filter_sources_dropdown.test.tsx | 39 ++++++ .../table_filter_users_dropdown.test.tsx | 30 +++++ .../groups/components/table_filters.test.tsx | 49 +++++++ .../components/user_option_item.test.tsx | 36 ++++++ 27 files changed, 1345 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts new file mode 100644 index 0000000000000..c4b129d870ea1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/groups.mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { contentSources } from './content_sources.mock'; +import { users } from './users.mock'; + +export const groups = [ + { + id: '123', + name: 'group', + createdAt: '2020-10-06', + updatedAt: '2020-10-06', + users, + usersCount: users.length, + color: 'motherofpearl', + contentSources, + canEditGroup: true, + canDeleteGroup: true, + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx new file mode 100644 index 0000000000000..59216126a2372 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues, setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AddGroupModal } from './add_group_modal'; + +import { EuiModal, EuiOverlayMask } from '@elastic/eui'; + +describe('AddGroupModal', () => { + const closeNewGroupModal = jest.fn(); + const saveNewGroup = jest.fn(); + const setNewGroupName = jest.fn(); + + beforeEach(() => { + setMockValues({ + newGroupNameErrors: [], + newGroupName: 'foo', + }); + + setMockActions({ + closeNewGroupModal, + saveNewGroup, + setNewGroupName, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiModal)).toHaveLength(1); + expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); + }); + + it('updates the input value', () => { + const wrapper = shallow(); + + const input = wrapper.find('[data-test-subj="AddGroupInput"]'); + input.simulate('change', { target: { value: 'bar' } }); + + expect(setNewGroupName).toHaveBeenCalledWith('bar'); + }); + + it('submits the form', () => { + const wrapper = shallow(); + + const simulatedEvent = { + form: 0, + target: { getAttribute: () => '_self' }, + preventDefault: jest.fn(), + }; + + const form = wrapper.find('form'); + form.simulate('submit', simulatedEvent); + expect(simulatedEvent.preventDefault).toHaveBeenCalled(); + expect(saveNewGroup).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx new file mode 100644 index 0000000000000..6a781f52c9e95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ClearFiltersLink } from './clear_filters_link'; + +import { EuiLink } from '@elastic/eui'; + +describe('ClearFiltersLink', () => { + const resetGroupsFilters = jest.fn(); + + beforeEach(() => { + setMockActions({ + resetGroupsFilters, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiLink)).toHaveLength(1); + }); + + it('handles click', () => { + const wrapper = shallow(); + + const button = wrapper.find(EuiLink); + button.simulate('click'); + + expect(resetGroupsFilters).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx new file mode 100644 index 0000000000000..f23a0c8d14042 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiFieldSearch, EuiFilterSelectItem, EuiCard, EuiPopoverTitle } from '@elastic/eui'; + +import { FilterableUsersList } from './filterable_users_list'; + +import { IUser } from '../../../types'; + +const mockSetState = jest.fn(); +const useStateMock: any = (initState: any) => [initState, mockSetState]; + +const addFilteredUser = jest.fn(); +const removeFilteredUser = jest.fn(); + +const props = { + users, + addFilteredUser, + removeFilteredUser, +}; + +describe('FilterableUsersList', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); + expect(wrapper.find(EuiCard)).toHaveLength(0); + }); + + it('updates the input value and renders zero users card', () => { + jest.spyOn(React, 'useState').mockImplementation(useStateMock); + const _users = [ + users[0], + { + ...users[0], + id: 'asdfa', + email: 'user@example.co', + name: null, + }, + ]; + + const wrapper = shallow(); + + const input = wrapper.find(EuiFieldSearch); + input.simulate('change', { target: { value: 'bar' } }); + + expect(wrapper.find(EuiCard)).toHaveLength(1); + expect(wrapper.find(EuiFilterSelectItem)).toHaveLength(0); + }); + + it('handles adding and removing users', () => { + const _users = [ + users[0], + { + ...users[0], + id: 'asdfa', + }, + ]; + + const wrapper = shallow( + + ); + const firstItem = wrapper.find(EuiFilterSelectItem).first(); + firstItem.simulate('click'); + + expect(removeFilteredUser).toHaveBeenCalled(); + expect(addFilteredUser).not.toHaveBeenCalled(); + + const secondItem = wrapper.find(EuiFilterSelectItem).last(); + secondItem.simulate('click'); + + expect(addFilteredUser).toHaveBeenCalled(); + }); + + it('renders loading when no users', () => { + const wrapper = shallow( + loading} /> + ); + const card = wrapper.find(EuiCard); + + expect((card.prop('description') as any).props.children).toEqual('loading'); + }); + + it('handles hidden users when count is higher than 20', () => { + const _users = [] as IUser[]; + const NUM_TOTAL_USERS = 30; + const NUM_VISIBLE_USERS = 20; + + [...Array(NUM_TOTAL_USERS)].forEach((_, i) => { + _users.push({ + ...users[0], + id: i.toString(), + }); + }); + + const wrapper = shallow(); + + expect(wrapper.find(EuiFilterSelectItem)).toHaveLength(NUM_VISIBLE_USERS); + }); + + it('renders elements wrapped when popover', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiPopoverTitle)).toHaveLength(1); + expect(wrapper.find('.euiFilterSelect__items')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx new file mode 100644 index 0000000000000..215a0e3eecdd8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { FilterableUsersPopover } from './filterable_users_popover'; +import { FilterableUsersList } from './filterable_users_list'; + +import { EuiFilterGroup, EuiPopover } from '@elastic/eui'; + +const closePopover = jest.fn(); +const addFilteredUser = jest.fn(); +const removeFilteredUser = jest.fn(); + +const props = { + users, + closePopover, + isPopoverOpen: false, + button: <>, +}; + +describe('FilterableUsersPopover', () => { + beforeEach(() => { + setMockActions({ + addFilteredUser, + removeFilteredUser, + }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFilterGroup)).toHaveLength(1); + expect(wrapper.find(EuiPopover)).toHaveLength(1); + expect(wrapper.find(FilterableUsersList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx new file mode 100644 index 0000000000000..2826d740d5339 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupManagerModal } from './group_manager_modal'; + +import { EuiOverlayMask, EuiModal, EuiEmptyPrompt } from '@elastic/eui'; + +const hideModal = jest.fn(); +const selectAll = jest.fn(); +const saveItems = jest.fn(); + +const props = { + children: <>, + label: 'shared content sources', + allItems: [], + numSelected: 1, + hideModal, + selectAll, + saveItems, +}; + +const mockValues = { + group: groups[0], + contentSources, + managerModalFormErrors: [], +}; + +describe('GroupManagerModal', () => { + beforeEach(() => { + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiModal)).toHaveLength(1); + expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ ...mockValues, contentSources: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('selects all items when clicked', () => { + const wrapper = shallow(); + + const button = wrapper.find('[data-test-subj="SelectAllGroups"]'); + button.simulate('click'); + + expect(selectAll).toHaveBeenCalledWith([]); + }); + + it('deselects all items when clicked', () => { + const wrapper = shallow(); + + const button = wrapper.find('[data-test-subj="SelectAllGroups"]'); + button.simulate('click'); + + expect(selectAll).toHaveBeenCalledWith([{}]); + }); + + it('handles cancel when clicked', () => { + const wrapper = shallow(); + + const button = wrapper.find('[data-test-subj="CloseGroupsModal"]'); + button.simulate('click'); + + expect(hideModal).toHaveBeenCalledWith(groups[0]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index db576808b66e3..c91516edf7b15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -139,7 +139,7 @@ export const GroupManagerModal: React.FC = ({ - + {i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupManagerSelectAllToggle', { @@ -152,7 +152,9 @@ export const GroupManagerModal: React.FC = ({ - {CANCEL_BUTTON_TEXT} + + {CANCEL_BUTTON_TEXT} + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx new file mode 100644 index 0000000000000..acb2fcfbaaa32 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + GroupOverview, + EMPTY_SOURCES_DESCRIPTION, + EMPTY_USERS_DESCRIPTION, +} from './group_overview'; + +import { ContentSection } from '../../../components/shared/content_section'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { SourcesTable } from '../../../components/shared/sources_table'; +import { Loading } from '../../../components/shared/loading'; + +import { EuiFieldText } from '@elastic/eui'; + +const deleteGroup = jest.fn(); +const showSharedSourcesModal = jest.fn(); +const showManageUsersModal = jest.fn(); +const showConfirmDeleteModal = jest.fn(); +const hideConfirmDeleteModal = jest.fn(); +const updateGroupName = jest.fn(); +const onGroupNameInputChange = jest.fn(); + +const mockValues = { + group: groups[0], + groupNameInputValue: '', + dataLoading: false, + confirmDeleteModalVisible: true, +}; + +describe('GroupOverview', () => { + beforeEach(() => { + setMockActions({ + deleteGroup, + showSharedSourcesModal, + showManageUsersModal, + showConfirmDeleteModal, + hideConfirmDeleteModal, + updateGroupName, + onGroupNameInputChange, + }); + setMockValues(mockValues); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ContentSection)).toHaveLength(4); + expect(wrapper.find(ViewContentHeader)).toHaveLength(1); + expect(wrapper.find(SourcesTable)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('updates the input value', () => { + const wrapper = shallow(); + + const input = wrapper.find(EuiFieldText); + input.simulate('change', { target: { value: 'bar' } }); + + expect(onGroupNameInputChange).toHaveBeenCalledWith('bar'); + }); + + it('submits the form', () => { + const wrapper = shallow(); + + const simulatedEvent = { + form: 0, + target: { getAttribute: () => '_self' }, + preventDefault: jest.fn(), + }; + + const form = wrapper.find('form'); + form.simulate('submit', simulatedEvent); + expect(simulatedEvent.preventDefault).toHaveBeenCalled(); + expect(updateGroupName).toHaveBeenCalled(); + }); + + it('renders empty state messages', () => { + setMockValues({ + ...mockValues, + group: { + ...groups[0], + users: [], + contentSources: [], + }, + }); + + const wrapper = shallow(); + const sourcesSection = wrapper.find('[data-test-subj="GroupContentSourcesSection"]') as any; + const usersSection = wrapper.find('[data-test-subj="GroupUsersSection"]') as any; + + expect(sourcesSection.prop('description')).toEqual(EMPTY_SOURCES_DESCRIPTION); + expect(usersSection.prop('description')).toEqual(EMPTY_USERS_DESCRIPTION); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index 1c7a01a1d9a46..fd97f1c0a03ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -33,7 +33,7 @@ import { GroupUsersTable } from './group_users_table'; import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; -const EMPTY_SOURCES_DESCRIPTION = i18n.translate( +export const EMPTY_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription', { defaultMessage: 'No content sources are shared with this group.', @@ -45,7 +45,7 @@ const GROUP_USERS_DESCRIPTION = i18n.translate( defaultMessage: 'Members will be able to search over the group’s sources.', } ); -const EMPTY_USERS_DESCRIPTION = i18n.translate( +export const EMPTY_USERS_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptyUsersDescription', { defaultMessage: 'There are no users in this group.', @@ -180,6 +180,7 @@ export const GroupOverview: React.FC = () => { title="Group content sources" description={hasContentSources ? GROUP_SOURCES_DESCRIPTION : EMPTY_SOURCES_DESCRIPTION} action={manageSourcesButton} + data-test-subj="GroupContentSourcesSection" > {hasContentSources && sourcesTable} @@ -190,6 +191,7 @@ export const GroupOverview: React.FC = () => { title="Group users" description={hasUsers ? GROUP_USERS_DESCRIPTION : EMPTY_USERS_DESCRIPTION} action={manageUsersButton} + data-test-subj="GroupUsersSection" > {hasUsers && } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx new file mode 100644 index 0000000000000..c7eea8ab64d45 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import moment from 'moment'; + +import { GroupRow, NO_USERS_MESSAGE, NO_SOURCES_MESSAGE } from './group_row'; +import { GroupUsers } from './group_users'; + +import { EuiTableRow } from '@elastic/eui'; + +describe('GroupRow', () => { + beforeEach(() => { + setMockValues({ isFederatedAuth: true }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + }); + + it('renders group users', () => { + setMockValues({ isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find(GroupUsers)).toHaveLength(1); + }); + + it('renders fromNow date string when in range', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('small').text()).toEqual('Last updated 7 days ago.'); + }); + + it('renders formatted date string when out of range', () => { + const wrapper = shallow(); + + expect(wrapper.find('small').text()).toEqual('Last updated January 1, 2020.'); + }); + + it('renders empty users message when no users present', () => { + setMockValues({ isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find('.user-group__accounts').text()).toEqual(NO_USERS_MESSAGE); + }); + + it('renders empty sources message when no sources present', () => { + const wrapper = shallow(); + + expect(wrapper.find('.user-group__sources').text()).toEqual(NO_SOURCES_MESSAGE); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 9c7276372cf54..9642d48af55f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -24,13 +24,13 @@ import { GroupSources } from './group_sources'; import { GroupUsers } from './group_users'; const DAYS_CUTOFF = 8; -const NO_SOURCES_MESSAGE = i18n.translate( +export const NO_SOURCES_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage', { defaultMessage: 'No shared content sources', } ); -const NO_USERS_MESSAGE = i18n.translate( +export const NO_USERS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage', { defaultMessage: 'No users', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx new file mode 100644 index 0000000000000..9493e52e08b81 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; +import { SourceOptionItem } from './source_option_item'; + +import { EuiFilterGroup } from '@elastic/eui'; + +const onButtonClick = jest.fn(); +const closePopover = jest.fn(); + +const props = { + isPopoverOpen: true, + numOptions: 1, + groupSources: contentSources, + onButtonClick, + closePopover, +}; + +describe('GroupRowSourcesDropdown', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceOptionItem)).toHaveLength(2); + expect(wrapper.find(EuiFilterGroup)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx new file mode 100644 index 0000000000000..039a2620f1fd4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { EuiLoadingContent, EuiButtonEmpty } from '@elastic/eui'; + +import { GroupRowUsersDropdown } from './group_row_users_dropdown'; +import { FilterableUsersPopover } from './filterable_users_popover'; + +const fetchGroupUsers = jest.fn(); +const onButtonClick = jest.fn(); +const closePopover = jest.fn(); + +const props = { + isPopoverOpen: true, + numOptions: 1, + groupId: '123', + onButtonClick, + closePopover, +}; +describe('GroupRowUsersDropdown', () => { + beforeEach(() => { + setMockActions({ fetchGroupUsers }); + setMockValues({ + allGroupUsers: users, + allGroupUsersLoading: false, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(FilterableUsersPopover)).toHaveLength(1); + }); + + it('handles toggle click', () => { + const wrapper = mount(); + + const button = wrapper.find(EuiButtonEmpty); + button.simulate('click'); + + expect(fetchGroupUsers).toHaveBeenCalledWith('123'); + expect(onButtonClick).toHaveBeenCalled(); + }); + + it('handles loading state', () => { + setMockValues({ + allGroupUsers: users, + allGroupUsersLoading: true, + }); + const wrapper = shallow(); + const popover = wrapper.find(FilterableUsersPopover); + + expect(popover.prop('allGroupUsersLoading')).toEqual(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx new file mode 100644 index 0000000000000..81639327f4ba0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Loading } from '../../../components/shared/loading'; + +import { GroupSourcePrioritization } from './group_source_prioritization'; + +import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui'; + +const updatePriority = jest.fn(); +const saveGroupSourcePrioritization = jest.fn(); +const showSharedSourcesModal = jest.fn(); + +const mockValues = { + group: groups[0], + activeSourcePriorities: [ + { + [groups[0].id]: 1, + }, + ], + dataLoading: false, + groupPrioritiesUnchanged: true, +}; + +describe('GroupSourcePrioritization', () => { + beforeEach(() => { + setMockActions({ + updatePriority, + saveGroupSourcePrioritization, + showSharedSourcesModal, + }); + + setMockValues(mockValues); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + }); + + it('returns loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ + ...mockValues, + group: { + ...groups[0], + contentSources: [], + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('handles slider change', () => { + const wrapper = shallow(); + + const slider = wrapper.find(EuiRange).first(); + slider.simulate('change', { target: { value: 2 } }); + + expect(updatePriority).toHaveBeenCalledWith('123', 2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx new file mode 100644 index 0000000000000..38c56aefebafd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupSources } from './group_sources'; +import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +import { IContentSourceDetails } from '../../../types'; + +describe('GroupSources', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourceIcon)).toHaveLength(2); + }); + + it('handles hidden sources when count is higer than 10', () => { + const sources = [] as IContentSourceDetails[]; + const NUM_TOTAL_SOURCES = 10; + + [...Array(NUM_TOTAL_SOURCES)].forEach((_, i) => { + sources.push({ + ...contentSources[0], + id: i.toString(), + }); + }); + + const wrapper = shallow(); + + // These were needed for 100% test coverage. + wrapper.find(GroupRowSourcesDropdown).invoke('onButtonClick')(); + wrapper.find(GroupRowSourcesDropdown).invoke('closePopover')(); + + expect(wrapper.find(GroupRowSourcesDropdown)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx new file mode 100644 index 0000000000000..7ddecc21c22c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { GroupSubNav } from './group_sub_nav'; + +import { SideNavLink } from '../../../../shared/layout'; + +describe('GroupSubNav', () => { + it('renders empty when no group id present', () => { + setMockValues({ group: {} }); + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink)).toHaveLength(0); + }); + + it('renders nav items', () => { + setMockValues({ group: { id: '1' } }); + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx new file mode 100644 index 0000000000000..6a635eacf2585 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { IUser } from '../../../types'; + +import { GroupUsers } from './group_users'; +import { GroupRowUsersDropdown } from './group_row_users_dropdown'; + +import { UserIcon } from '../../../components/shared/user_icon'; + +const props = { + groupUsers: users, + usersCount: 1, + groupId: '123', +}; + +describe('GroupUsers', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(UserIcon)).toHaveLength(1); + }); + + it('handles hidden users when count is higher than 20', () => { + const _users = [] as IUser[]; + const NUM_TOTAL_USERS = 20; + + [...Array(NUM_TOTAL_USERS)].forEach((_, i) => { + _users.push({ + ...users[0], + id: i.toString(), + }); + }); + + const wrapper = shallow(); + + // These were needed for 100% test coverage. + wrapper.find(GroupRowUsersDropdown).invoke('onButtonClick')(); + wrapper.find(GroupRowUsersDropdown).invoke('closePopover')(); + + expect(wrapper.find(GroupRowUsersDropdown)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx new file mode 100644 index 0000000000000..8747a838689c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { IUser } from '../../../types'; + +import { GroupUsersTable } from './group_users_table'; +import { TableHeader } from '../../../../shared/table_header'; + +import { EuiTable, EuiTablePagination } from '@elastic/eui'; + +const group = groups[0]; + +describe('GroupUsersTable', () => { + it('renders', () => { + setMockValues({ isFederatedAuth: true, group }); + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(TableHeader).prop('headerItems')).toHaveLength(1); + }); + + it('adds header item for non-federated auth', () => { + setMockValues({ isFederatedAuth: false, group }); + const wrapper = shallow(); + + expect(wrapper.find(TableHeader).prop('headerItems')).toHaveLength(2); + }); + + it('renders pagination', () => { + const users = [] as IUser[]; + const NUM_TOTAL_USERS = 20; + + [...Array(NUM_TOTAL_USERS)].forEach((_, i) => { + users.push({ + ...group.users[0], + id: i.toString(), + }); + }); + + setMockValues({ isFederatedAuth: true, group: { users } }); + const wrapper = shallow(); + const pagination = wrapper.find(EuiTablePagination); + + // This was needed for 100% test coverage. The tests pass and 100% coverage + // is achieved with this line, but TypeScript complains anyway, so ignoring line. + // @ts-ignore + pagination.invoke('onChangePage')(1); + + expect(pagination).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx new file mode 100644 index 0000000000000..38d035cbca908 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import { DEFAULT_META } from '../../../../shared/constants'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; + +import { GroupsTable } from './groups_table'; +import { GroupRow } from './group_row'; +import { ClearFiltersLink } from './clear_filters_link'; + +import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; + +const setActivePage = jest.fn(); + +const mockValues = { + groupsMeta: DEFAULT_META, + groups, + hasFiltersSet: false, + isFederatedAuth: true, +}; + +describe('GroupsTable', () => { + beforeEach(() => { + setMockActions({ setActivePage }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiTable)).toHaveLength(1); + expect(wrapper.find(GroupRow)).toHaveLength(1); + }); + + it('renders extra header for non-federated auth', () => { + setMockValues({ ...mockValues, isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(4); + }); + + it('handles pagination', () => { + setMockValues({ + ...mockValues, + groupsMeta: { + page: { + current: 1, + size: 10, + total_pages: 3, + total_results: 30, + }, + }, + }); + + const wrapper = shallow(); + wrapper.find(TablePaginationBar).first().invoke('onChangePage')(1); + + expect(setActivePage).toHaveBeenCalledWith(2); + expect(wrapper.find(TablePaginationBar)).toHaveLength(2); + }); + + it('renders clear filters link when filters set', () => { + setMockValues({ ...mockValues, hasFiltersSet: true }); + const wrapper = shallow(); + + expect(wrapper.find(ClearFiltersLink)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx new file mode 100644 index 0000000000000..34f748e8a7169 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ManageUsersModal } from './manage_users_modal'; +import { FilterableUsersList } from './filterable_users_list'; +import { GroupManagerModal } from './group_manager_modal'; + +const addGroupUser = jest.fn(); +const removeGroupUser = jest.fn(); +const selectAllUsers = jest.fn(); +const hideManageUsersModal = jest.fn(); +const saveGroupUsers = jest.fn(); + +describe('ManageUsersModal', () => { + it('renders', () => { + setMockActions({ + addGroupUser, + removeGroupUser, + selectAllUsers, + hideManageUsersModal, + saveGroupUsers, + }); + + setMockValues({ + users, + selectedGroupUsers: [], + }); + + const wrapper = shallow(); + + expect(wrapper.find(FilterableUsersList)).toHaveLength(1); + expect(wrapper.find(GroupManagerModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx new file mode 100644 index 0000000000000..8c5ead2509d9e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { groups } from '../../../__mocks__/groups.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SharedSourcesModal } from './shared_sources_modal'; +import { GroupManagerModal } from './group_manager_modal'; +import { SourcesList } from './sources_list'; + +const group = groups[0]; + +const addGroupSource = jest.fn(); +const selectAllSources = jest.fn(); +const hideSharedSourcesModal = jest.fn(); +const removeGroupSource = jest.fn(); +const saveGroupSources = jest.fn(); + +describe('SharedSourcesModal', () => { + it('renders', () => { + setMockActions({ + addGroupSource, + selectAllSources, + hideSharedSourcesModal, + removeGroupSource, + saveGroupSources, + }); + + setMockValues({ + group, + selectedGroupSources: [], + contentSources: group.contentSources, + }); + + const wrapper = shallow(); + + expect(wrapper.find(SourcesList)).toHaveLength(1); + expect(wrapper.find(GroupManagerModal)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx new file mode 100644 index 0000000000000..8a3901f5462df --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SourceOptionItem } from './source_option_item'; + +import { TruncatedContent } from '../../../../shared/truncate'; + +import { SourceIcon } from '../../../components/shared/source_icon'; + +describe('SourceOptionItem', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(TruncatedContent)).toHaveLength(1); + expect(wrapper.find(SourceIcon)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx new file mode 100644 index 0000000000000..05754f3846bb0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SourcesList } from './sources_list'; + +import { EuiFilterSelectItem } from '@elastic/eui'; + +const addFilteredSource = jest.fn(); +const removeFilteredSource = jest.fn(); + +const props = { + contentSources, + filteredSources: [], + addFilteredSource, + removeFilteredSource, +}; + +describe('SourcesList', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFilterSelectItem)).toHaveLength(2); + }); + + it('handles adding item click when item unchecked', () => { + const wrapper = shallow(); + wrapper.find(EuiFilterSelectItem).first().simulate('click'); + + expect(addFilteredSource).toHaveBeenCalled(); + }); + + it('handles removing item click when item checked', () => { + const wrapper = shallow(); + wrapper.find(EuiFilterSelectItem).first().simulate('click'); + + expect(removeFilteredSource).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx new file mode 100644 index 0000000000000..e75feb4254929 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { contentSources } from '../../../__mocks__/content_sources.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; + +import { SourcesList } from './sources_list'; + +const addFilteredSource = jest.fn(); +const removeFilteredSource = jest.fn(); +const toggleFilterSourcesDropdown = jest.fn(); +const closeFilterSourcesDropdown = jest.fn(); + +describe('TableFilterSourcesDropdown', () => { + it('renders', () => { + setMockActions({ + addFilteredSource, + removeFilteredSource, + toggleFilterSourcesDropdown, + closeFilterSourcesDropdown, + }); + + setMockValues({ contentSources, filterSourcesDropdownOpen: false, filteredSources: [] }); + + const wrapper = shallow(); + + expect(wrapper.find(SourcesList)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx new file mode 100644 index 0000000000000..9d461e06a77ec --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; +import { FilterableUsersPopover } from './filterable_users_popover'; + +const closeFilterUsersDropdown = jest.fn(); +const toggleFilterUsersDropdown = jest.fn(); + +describe('TableFilterUsersDropdown', () => { + it('renders', () => { + setMockActions({ closeFilterUsersDropdown, toggleFilterUsersDropdown }); + setMockValues({ users, filteredUsers: [], filterUsersDropdownOpen: false }); + + const wrapper = shallow(); + + expect(wrapper.find(FilterableUsersPopover)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx new file mode 100644 index 0000000000000..80662bc0974a1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { setMockActions, setMockValues } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TableFilters } from './table_filters'; + +import { EuiFieldSearch } from '@elastic/eui'; +import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; +import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; + +const setFilterValue = jest.fn(); + +describe('TableFilters', () => { + beforeEach(() => { + setMockValues({ filterValue: '', isFederatedAuth: true }); + setMockActions({ setFilterValue }); + }); + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldSearch)).toHaveLength(1); + expect(wrapper.find(TableFilterSourcesDropdown)).toHaveLength(1); + expect(wrapper.find(TableFilterUsersDropdown)).toHaveLength(0); + }); + + it('renders for non-federated Auth', () => { + setMockValues({ filterValue: '', isFederatedAuth: false }); + const wrapper = shallow(); + + expect(wrapper.find(TableFilterUsersDropdown)).toHaveLength(1); + }); + + it('handles search input value change', () => { + const wrapper = shallow(); + const input = wrapper.find(EuiFieldSearch); + input.simulate('change', { target: { value: 'bar' } }); + + expect(setFilterValue).toHaveBeenCalledWith('bar'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx new file mode 100644 index 0000000000000..72611f254d01c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/kea.mock'; + +import { users } from '../../../__mocks__/users.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UserOptionItem } from './user_option_item'; +import { UserIcon } from '../../../components/shared/user_icon'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +const user = users[0]; + +describe('UserOptionItem', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(UserIcon)).toHaveLength(1); + expect(wrapper.find(EuiFlexGroup)).toHaveLength(1); + expect(wrapper.find(EuiFlexItem)).toHaveLength(2); + }); + + it('falls back to email when name not present', () => { + const wrapper = shallow(); + const nameItem = wrapper.find(EuiFlexItem).last(); + + expect(nameItem.prop('children')).toEqual(user.email); + }); +}); From a5a42fdf709cd40f97e207e4ad20f936d9186aa4 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 16 Oct 2020 12:06:12 -0400 Subject: [PATCH 59/79] [Expressions] Finish connecting debug mode on expressions (#78874) * [Expressions] Finish connecting debug mode on expressions * Fix bad merge * Commit API docs --- ...ugin-plugins-expressions-public.executor.md | 2 +- ...-plugins-expressions-public.executor.run.md | 3 ++- ...s-public.expressionsservicestart.execute.md | 2 +- ...pressions-public.expressionsservicestart.md | 4 ++-- ...sions-public.expressionsservicestart.run.md | 2 +- ...ons-public.iexpressionloaderparams.debug.md | 11 +++++++++++ ...pressions-public.iexpressionloaderparams.md | 1 + ...ugin-plugins-expressions-server.executor.md | 2 +- ...-plugins-expressions-server.executor.run.md | 3 ++- .../expressions/common/executor/executor.ts | 9 +++++++-- .../common/service/expressions_services.ts | 16 +++++++++------- src/plugins/expressions/public/loader.ts | 18 +++++++++++++----- src/plugins/expressions/public/public.api.md | 8 +++++--- src/plugins/expressions/public/types/index.ts | 2 ++ src/plugins/expressions/server/server.api.md | 2 +- 15 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md index 2f96ad6e040bd..013624f30b45a 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executor.md @@ -41,6 +41,6 @@ export declare class Executor = RecordSignature: ```typescript -run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; +run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; ``` ## Parameters @@ -19,6 +19,7 @@ run = Recordstring | ExpressionAstExpression | | | input | Input | | | context | ExtraContext | | +| options | ExpressionExecOptions | | Returns: diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md index b8211a6bff27c..18b856b946da4 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md @@ -9,5 +9,5 @@ Starts expression execution and immediately returns `ExecutionContract` instance Signature: ```typescript -execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => ExecutionContract; +execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md index 34bf16c121326..def572abead22 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.md @@ -16,12 +16,12 @@ export interface ExpressionsServiceStart | Property | Type | Description | | --- | --- | --- | -| [execute](./kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md) | <Input = unknown, Output = unknown, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => ExecutionContract<ExtraContext, Input, Output> | Starts expression execution and immediately returns ExecutionContract instance that tracks the progress of the execution and can be used to interact with the execution. | +| [execute](./kibana-plugin-plugins-expressions-public.expressionsservicestart.execute.md) | <Input = unknown, Output = unknown, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract<ExtraContext, Input, Output> | Starts expression execution and immediately returns ExecutionContract instance that tracks the progress of the execution and can be used to interact with the execution. | | [fork](./kibana-plugin-plugins-expressions-public.expressionsservicestart.fork.md) | () => ExpressionsService | Create a new instance of ExpressionsService. The new instance inherits all state of the original ExpressionsService, including all expression types, expression functions and context. Also, all new types and functions registered in the original services AFTER the forking event will be available in the forked instance. However, all new types and functions registered in the forked instances will NOT be available to the original service. | | [getFunction](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getfunction.md) | (name: string) => ReturnType<Executor['getFunction']> | Get a registered ExpressionFunction by its name, which was registered using the registerFunction method. The returned ExpressionFunction instance is an internal representation of the function in Expressions service - do not mutate that object. | | [getRenderer](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getrenderer.md) | (name: string) => ReturnType<ExpressionRendererRegistry['get']> | Get a registered ExpressionRenderer by its name, which was registered using the registerRenderer method. The returned ExpressionRenderer instance is an internal representation of the renderer in Expressions service - do not mutate that object. | | [getType](./kibana-plugin-plugins-expressions-public.expressionsservicestart.gettype.md) | (name: string) => ReturnType<Executor['getType']> | Get a registered ExpressionType by its name, which was registered using the registerType method. The returned ExpressionType instance is an internal representation of the type in Expressions service - do not mutate that object. | -| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => Promise<Output> | Executes expression string or a parsed expression AST and immediately returns the result.Below example will execute sleep 100 | clog expression with 123 initial input to the first function. +| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <Input, Output, ExtraContext extends Record<string, unknown> = Record<string, unknown>>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise<Output> | Executes expression string or a parsed expression AST and immediately returns the result.Below example will execute sleep 100 | clog expression with 123 initial input to the first function. ```ts expressions.run('sleep 100 | clog', 123); diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md index 578c583624ad0..d717af51a00fa 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md @@ -24,5 +24,5 @@ expressions.run('...', null, { elasticsearchClient }); Signature: ```typescript -run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => Promise; +run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md new file mode 100644 index 0000000000000..b27246449cc7c --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) + +## IExpressionLoaderParams.debug property + +Signature: + +```typescript +debug?: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index b8a174f93fb99..d6e02350bae3f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -17,6 +17,7 @@ export interface IExpressionLoaderParams | [context](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.context.md) | ExpressionValue | | | [customFunctions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customfunctions.md) | [] | | | [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | [] | | +| [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean | | | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md index ec4e0bdcc4569..46ad60ae07126 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executor.md @@ -41,6 +41,6 @@ export declare class Executor = RecordSignature: ```typescript -run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; +run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; ``` ## Parameters @@ -19,6 +19,7 @@ run = Recordstring | ExpressionAstExpression | | | input | Input | | | context | ExtraContext | | +| options | ExpressionExecOptions | | Returns: diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 28aae8c8f4834..fd7f5808f0340 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -170,8 +170,13 @@ export class Executor = Record = Record - >(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) { - const execution = this.createExecution(ast, context); + >( + ast: string | ExpressionAstExpression, + input: Input, + context?: ExtraContext, + options?: ExpressionExecOptions + ) { + const execution = this.createExecution(ast, context, options); execution.start(input); return (await execution.result) as Output; } diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 4a87fd9e7f331..3d0fb968e8a3a 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Executor } from '../executor'; +import { Executor, ExpressionExecOptions } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; import { ExpressionAstExpression } from '../ast'; import { ExecutionContract } from '../execution/execution_contract'; @@ -101,7 +101,8 @@ export interface ExpressionsServiceStart { run: = Record>( ast: string | ExpressionAstExpression, input: Input, - context?: ExtraContext + context?: ExtraContext, + options?: ExpressionExecOptions ) => Promise; /** @@ -117,7 +118,8 @@ export interface ExpressionsServiceStart { ast: string | ExpressionAstExpression, // This any is for legacy reasons. input: Input, - context?: ExtraContext + context?: ExtraContext, + options?: ExpressionExecOptions ) => ExecutionContract; /** @@ -212,8 +214,8 @@ export class ExpressionsService implements PersistableState AnyExpressionRenderDefinition) ): void => this.renderers.register(definition); - public readonly run: ExpressionsServiceStart['run'] = (ast, input, context) => - this.executor.run(ast, input, context); + public readonly run: ExpressionsServiceStart['run'] = (ast, input, context, options) => + this.executor.run(ast, input, context, options); public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) => this.executor.getFunction(name); @@ -244,8 +246,8 @@ export class ExpressionsService implements PersistableState => this.executor.getTypes(); - public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, context) => { - const execution = this.executor.createExecution(ast, context); + public readonly execute: ExpressionsServiceStart['execute'] = ((ast, input, context, options) => { + const execution = this.executor.createExecution(ast, context, options); execution.start(input); return execution.contract; }) as ExpressionsServiceStart['execute']; diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index c4c40e0812e48..aef4b73f86e34 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -145,11 +145,18 @@ export class ExpressionLoader { this.execution.cancel(); } this.setParams(params); - this.execution = getExpressionsService().execute(expression, params.context, { - search: params.searchContext, - variables: params.variables || {}, - inspectorAdapters: params.inspectorAdapters, - }); + this.execution = getExpressionsService().execute( + expression, + params.context, + { + search: params.searchContext, + variables: params.variables || {}, + inspectorAdapters: params.inspectorAdapters, + }, + { + debug: params.debug, + } + ); const prevDataHandler = this.execution; const data = await prevDataHandler.getData(); @@ -181,6 +188,7 @@ export class ExpressionLoader { if (params.variables && this.params) { this.params.variables = params.variables; } + this.params.debug = Boolean(params.debug); this.params.inspectorAdapters = (params.inspectorAdapters || this.execution?.inspect()) as Adapters; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index c7b6190b96ed7..3f74fe045dd2c 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -228,7 +228,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; + run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; // (undocumented) readonly state: ExecutorContainer; // (undocumented) @@ -609,12 +609,12 @@ export type ExpressionsServiceSetup = Pick = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => ExecutionContract; + execute: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => ExecutionContract; fork: () => ExpressionsService; getFunction: (name: string) => ReturnType; getRenderer: (name: string) => ReturnType; getType: (name: string) => ReturnType; - run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) => Promise; + run: = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions) => Promise; } // Warning: (ae-missing-release-tag) "ExpressionsSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -878,6 +878,8 @@ export interface IExpressionLoaderParams { // (undocumented) customRenderers?: []; // (undocumented) + debug?: boolean; + // (undocumented) disableCaching?: boolean; // (undocumented) inspectorAdapters?: Adapters; diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 37a4f4fee6336..054c5ac3dc467 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -45,6 +45,8 @@ export interface IExpressionLoaderParams { searchContext?: ExecutionContextSearch; context?: ExpressionValue; variables?: Record; + // Enables debug tracking on each expression in the AST + debug?: boolean; disableCaching?: boolean; customFunctions?: []; customRenderers?: []; diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index fd6cc8d742cd4..0c9e4af84f6a5 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -210,7 +210,7 @@ export class Executor = Record AnyExpressionFunctionDefinition)): void; // (undocumented) registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void; - run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext): Promise; + run = Record>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext, options?: ExpressionExecOptions): Promise; // (undocumented) readonly state: ExecutorContainer; // (undocumented) From a23b42301bffdc33373bd190b550972fe6870128 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 16 Oct 2020 18:06:50 +0200 Subject: [PATCH 60/79] [Discover] fix auto-refresh (#80635) * fix refresh interval in discover * Also implicitly fixes a subtle bug with excessive re-fetch after deleting already disabled filter Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../query_string/query_string_manager.test.ts | 52 +++++++++++++++++++ .../query_string/query_string_manager.ts | 4 +- .../public/application/angular/discover.js | 18 ++++--- test/functional/apps/discover/_discover.js | 33 ++++++++++++ test/functional/apps/visualize/_line_chart.js | 13 ++--- test/functional/page_objects/time_picker.ts | 11 ++++ 6 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 src/plugins/data/public/query/query_string/query_string_manager.test.ts diff --git a/src/plugins/data/public/query/query_string/query_string_manager.test.ts b/src/plugins/data/public/query/query_string/query_string_manager.test.ts new file mode 100644 index 0000000000000..aa1556480452a --- /dev/null +++ b/src/plugins/data/public/query/query_string/query_string_manager.test.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { QueryStringManager } from './query_string_manager'; +import { Storage } from '../../../../kibana_utils/public/storage'; +import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; +import { coreMock } from '../../../../../core/public/mocks'; +import { Query } from '../../../common/query'; + +describe('QueryStringManager', () => { + let service: QueryStringManager; + + beforeEach(() => { + service = new QueryStringManager( + new Storage(new StubBrowserStorage()), + coreMock.createSetup().uiSettings + ); + }); + + test('getUpdates$ is a cold emits only after query changes', () => { + const obs$ = service.getUpdates$(); + const emittedValues: Query[] = []; + obs$.subscribe((v) => { + emittedValues.push(v); + }); + expect(emittedValues).toHaveLength(0); + + const newQuery = { query: 'new query', language: 'kquery' }; + service.setQuery(newQuery); + expect(emittedValues).toHaveLength(1); + expect(emittedValues[0]).toEqual(newQuery); + + service.setQuery({ ...newQuery }); + expect(emittedValues).toHaveLength(1); + }); +}); diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index bd02830f4aed8..50732c99a62d9 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -17,8 +17,8 @@ * under the License. */ -import _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; +import { skip } from 'rxjs/operators'; import { CoreStart } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { Query, UI_SETTINGS } from '../../../common'; @@ -61,7 +61,7 @@ export class QueryStringManager { } public getUpdates$ = () => { - return this.query$.asObservable(); + return this.query$.asObservable().pipe(skip(1)); }; public getQuery = (): Query => { diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 659b8dede564c..612cedb7780bd 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -193,7 +193,7 @@ app.directive('discoverApp', function () { function discoverController($element, $route, $scope, $timeout, $window, Promise, uiCapabilities) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); - const $fetchObservable = new Subject(); + const refetch$ = new Subject(); let inspectorRequest; const savedSearch = $route.current.locals.savedObjects.savedSearch; $scope.searchSource = savedSearch.searchSource; @@ -267,7 +267,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise ); if (changes.length) { - $fetchObservable.next(); + refetch$.next(); } }); } @@ -634,12 +634,18 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise const init = _.once(() => { $scope.updateDataSource().then(async () => { - const searchBarChanges = merge(data.query.state$, $fetchObservable).pipe(debounceTime(100)); + const fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getAutoRefreshFetch$(), + data.query.queryString.getUpdates$() + ).pipe(debounceTime(100)); subscriptions.add( subscribeWithScope( $scope, - searchBarChanges, + fetch$, { next: $scope.fetch, }, @@ -718,7 +724,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise init.complete = true; if (shouldSearchOnPageLoad()) { - $fetchObservable.next(); + refetch$.next(); } }); }); @@ -816,7 +822,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise $scope.handleRefresh = function (_payload, isUpdate) { if (isUpdate === false) { - $fetchObservable.next(); + refetch$.next(); } }; diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index faf272daba097..e597cc14654bc 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); + const inspector = getService('inspector'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'logstash-*', @@ -292,5 +293,37 @@ export default function ({ getService, getPageObjects }) { expect(currentUrlWithoutScore).not.to.contain('_score'); }); }); + + describe('refresh interval', function () { + it('should refetch when autofresh is enabled', async () => { + const intervalS = 5; + await PageObjects.timePicker.startAutoRefresh(intervalS); + + // check inspector panel request stats for timestamp + await inspector.open(); + + const getRequestTimestamp = async () => { + const requestStats = await inspector.getTableData(); + const requestTimestamp = requestStats.filter((r) => + r[0].includes('Request timestamp') + )[0][1]; + return requestTimestamp; + }; + + const requestTimestampBefore = await getRequestTimestamp(); + await retry.waitFor('refetch because of refresh interval', async () => { + const requestTimestampAfter = await getRequestTimestamp(); + log.debug( + `Timestamp before: ${requestTimestampBefore}, Timestamp after: ${requestTimestampAfter}` + ); + return requestTimestampBefore !== requestTimestampAfter; + }); + }); + + after(async () => { + await inspector.close(); + await PageObjects.timePicker.pauseAutoRefresh(); + }); + }); }); } diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 24e4ef4a7fe25..8dfc4d352b133 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -136,15 +136,8 @@ export default function ({ getService, getPageObjects }) { }); it('should request new data when autofresh is enabled', async () => { - // enable autorefresh - const interval = 3; - await PageObjects.timePicker.openQuickSelectTimeMenu(); - await PageObjects.timePicker.inputValue( - 'superDatePickerRefreshIntervalInput', - interval.toString() - ); - await testSubjects.click('superDatePickerToggleRefreshButton'); - await PageObjects.timePicker.closeQuickSelectTimeMenu(); + const intervalS = 3; + await PageObjects.timePicker.startAutoRefresh(intervalS); // check inspector panel request stats for timestamp await inspector.open(); @@ -155,7 +148,7 @@ export default function ({ getService, getPageObjects }) { )[0][1]; // pause to allow time for autorefresh to fire another request - await PageObjects.common.sleep(interval * 1000 * 1.5); + await PageObjects.common.sleep(intervalS * 1000 * 1.5); // get the latest timestamp from request stats const requestStatsAfter = await inspector.getTableData(); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 237dc8946ae0e..3ac6c83e61f14 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -269,6 +269,17 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo return moment.duration(endMoment.diff(startMoment)).asHours(); } + public async startAutoRefresh(intervalS = 3) { + await this.openQuickSelectTimeMenu(); + await this.inputValue('superDatePickerRefreshIntervalInput', intervalS.toString()); + const refreshConfig = await this.getRefreshConfig(true); + if (refreshConfig.isPaused) { + log.debug('start auto refresh'); + await testSubjects.click('superDatePickerToggleRefreshButton'); + } + await this.closeQuickSelectTimeMenu(); + } + public async pauseAutoRefresh() { log.debug('pauseAutoRefresh'); const refreshConfig = await this.getRefreshConfig(true); From d4530e15c602f6415adff82e17ae658f0e5720c8 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 16 Oct 2020 18:16:10 +0200 Subject: [PATCH 61/79] [Docs] Document Encrypted Saved Objects functionality. (#80183) --- docs/api/saved-objects.asciidoc | 3 + .../rotate_encryption_key.asciidoc | 110 ++++++++++++++++++ docs/settings/security-settings.asciidoc | 23 ++++ .../security/secure-saved-objects.asciidoc | 47 ++++++++ docs/user/security/securing-kibana.asciidoc | 1 + 5 files changed, 184 insertions(+) create mode 100644 docs/api/saved-objects/rotate_encryption_key.asciidoc create mode 100644 docs/user/security/secure-saved-objects.asciidoc diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index a4e9fa32f8a5c..0d8ceefb47e91 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -28,6 +28,8 @@ The following saved objects APIs are available: * <> to resolve errors from the import API +* <> to rotate the encryption key for encrypted saved objects + include::saved-objects/get.asciidoc[] include::saved-objects/bulk_get.asciidoc[] include::saved-objects/find.asciidoc[] @@ -38,3 +40,4 @@ include::saved-objects/delete.asciidoc[] include::saved-objects/export.asciidoc[] include::saved-objects/import.asciidoc[] include::saved-objects/resolve_import_errors.asciidoc[] +include::saved-objects/rotate_encryption_key.asciidoc[] diff --git a/docs/api/saved-objects/rotate_encryption_key.asciidoc b/docs/api/saved-objects/rotate_encryption_key.asciidoc new file mode 100644 index 0000000000000..0a66ed2b4b361 --- /dev/null +++ b/docs/api/saved-objects/rotate_encryption_key.asciidoc @@ -0,0 +1,110 @@ +[role="xpack"] +[[saved-objects-api-rotate-encryption-key]] +=== Rotate encryption key API +++++ +Rotate encryption key +++++ + +experimental[] Rotate the encryption key for encrypted saved objects. + +If a saved object cannot be decrypted using the primary encryption key, then {kib} will attempt to decrypt it using the specified <>. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key. + +[IMPORTANT] +============================================================================ +Bulk key rotation can consume a considerable amount of resources and hence only user with a `superuser` role can trigger it. +============================================================================ + +[[saved-objects-api-rotate-encryption-key-request]] +==== Request + +`POST :/api/encrypted_saved_objects/_rotate_key` + +[[saved-objects-api-rotate-encryption-key-request-query-params]] +==== Query parameters + +`type`:: +(Optional, string) Limits encryption key rotation only to the saved objects with the specified type. By default, {kib} tries to rotate the encryption key for all saved object types that may contain encrypted attributes. + +`batchSize`:: +(Optional, number) Specifies a maximum number of saved objects that {kib} can process in a single batch. Bulk key rotation is an iterative process since {kib} may not be able to fetch and process all required saved objects in one go and splits processing into consequent batches. By default, the batch size is 10000, which is also a maximum allowed value. + +[[saved-objects-api-rotate-encryption-key-response-body]] +==== Response body + +`total`:: +(number) Indicates the total number of _all_ encrypted saved objects (optionally filtered by the requested `type`), regardless of the key {kib} used for encryption. + +`successful`:: +(number) Indicates the total number of _all_ encrypted saved objects (optionally filtered by the requested `type`), regardless of the key {kib} used for encryption. ++ +NOTE: In most cases, `total` will be greater than `successful` even if `failed` is zero. The reason is that {kib} may not need or may not be able to rotate encryption keys for all encrypted saved objects. + +`failed`:: +(number) Indicates the number of the saved objects that were still encrypted with one of the old encryption keys that {kib} failed to re-encrypt with the primary key. + +[[saved-objects-api-rotate-encryption-key-response-codes]] +==== Response code + +`200`:: +Indicates a successful call. + +`400`:: +Indicates that either query parameters are wrong or <> aren't configured. + +`429`:: +Indicates that key rotation is already in progress. + +[[saved-objects-api-rotate-encryption-key-example]] +==== Examples + +[[saved-objects-api-rotate-encryption-key-example-1]] +===== Encryption key rotation with default parameters + +[source,sh] +-------------------------------------------------- +$ curl -X POST /api/encrypted_saved_objects/_rotate_key +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "total": 1000, + "successful": 300, + "failed": 0 +} +-------------------------------------------------- + +The result indicates that the encryption key was successfully rotated for 300 out of 1000 saved objects with encrypted attributes, and 700 of the saved objects either didn't require key rotation, or were encrypted with an unknown encryption key. + +[[saved-objects-api-rotate-encryption-key-example-2]] +===== Encryption key rotation for the specific type with reduce batch size + +[IMPORTANT] +============================================================================ +Default parameters are optimized for speed. Change the parameters only when necessary. However, if you're experiencing any issues with this API, you may want to decrease a batch size or rotate the encryption keys for the specific types only. In this case, you may need to run key rotation multiple times in a row. +============================================================================ + +In this example, key rotation is performed for all saved objects with the `alert` type in batches of 5000. + +[source,sh] +-------------------------------------------------- +$ curl -X POST /api/encrypted_saved_objects/_rotate_key?type=alert&batchSize=5000 +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "total": 100, + "successful": 100, + "failed": 0 +} +-------------------------------------------------- + +The result indicates that the encryption key was successfully rotated for all 100 saved objects with the `alert` type. + diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 00e5f973f7d87..c743aa43fab05 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -239,3 +239,26 @@ The format is a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', ============ |=== + +[float] +[[security-encrypted-saved-objects-settings]] +==== Encrypted saved objects settings + +These settings control the encryption of saved objects with sensitive data. For more details, refer to <>. + +[IMPORTANT] +============ +In high-availability deployments, make sure you use the same encryption and decryption keys for all instances of {kib}. Although the keys can be specified in clear text in `kibana.yml`, it's recommended to store them securely in the <>. +============ + +[cols="2*<"] +|=== +| [[xpack-encryptedSavedObjects-encryptionKey]] `xpack.encryptedSavedObjects.` +`encryptionKey` +| An arbitrary string of at least 32 characters that is used to encrypt sensitive properties of saved objects before they're stored in {es}. If not set, {kib} will generate a random key on startup, but certain features won't be available until you set the encryption key explicitly. + +| [[xpack-encryptedSavedObjects-keyRotation-decryptionOnlyKeys]] `xpack.encryptedSavedObjects.` +`keyRotation.decryptionOnlyKeys` +| An optional list of previously used encryption keys. Like <>, these must be at least 32 characters in length. {kib} doesn't use these keys for encryption, but may still require them to decrypt some existing saved objects. Use this setting if you wish to change your encryption key, but don't want to lose access to saved objects that were previously encrypted with a different key. + +|=== \ No newline at end of file diff --git a/docs/user/security/secure-saved-objects.asciidoc b/docs/user/security/secure-saved-objects.asciidoc new file mode 100644 index 0000000000000..3b15a576500f1 --- /dev/null +++ b/docs/user/security/secure-saved-objects.asciidoc @@ -0,0 +1,47 @@ +[role="xpack"] +[[xpack-security-secure-saved-objects]] +=== Secure saved objects + +{kib} stores entities such as dashboards, visualizations, alerts, actions, and advanced settings as saved objects, which are kept in a dedicated, internal {es} index. If such an object includes sensitive information, for example a PagerDuty integration key or email server credentials used by the alert action, {kib} encrypts it and makes sure it cannot be accidentally leaked or tampered with. + +Encrypting sensitive information means that a malicious party with access to the {kib} internal indices won't be able to extract that information without also knowing the encryption key. + +Example `kibana.yml`: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.encryptedSavedObjects: + encryptionKey: "min-32-byte-long-strong-encryption-key" +-------------------------------------------------------------------------------- + +[IMPORTANT] +============================================================================ +If you don't specify an encryption key, {kib} automatically generates a random key at startup. Every time you restart {kib}, it uses a new ephemeral encryption key and is unable to decrypt saved objects encrypted with another key. To prevent data loss, {kib} might disable features that rely on this encryption until you explicitly set an encryption key. +============================================================================ + +[[encryption-key-rotation]] +==== Encryption key rotation + +Many policies and best practices stipulate that encryption keys should be periodically rotated to decrease the amount of content encrypted with one key and therefore limit the potential damage if the key is compromised. {kib} allows you to rotate encryption keys whenever there is a need. + +When you change an encryption key, be sure to keep the old one for some time. Although {kib} only uses a new encryption key to encrypt all new and updated data, it still may need the old one to decrypt data that was encrypted using the old key. It's possible to have multiple old keys used only for decryption. {kib} doesn't automatically re-encrypt existing saved objects with the new encryption key. Re-encryption only happens when you update existing object or use the <>. + +Here is how your `kibana.yml` might look if you use key rotation functionality: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.encryptedSavedObjects: + encryptionKey: "min-32-byte-long-NEW-encryption-key" <1> + keyRotation: + decryptionOnlyKeys: ["min-32-byte-long-OLD#1-encryption-key", "min-32-byte-long-OLD#2-encryption-key"] <2> +-------------------------------------------------------------------------------- + +<1> The encryption key {kib} will use to encrypt all new or updated saved objects. This is known as the primary encryption key. +<2> A list of encryption keys {kib} will try to use to decrypt existing saved objects if decryption with the primary encryption key isn't possible. These keys are known as the decryption-only or secondary encryption keys. + +[NOTE] +============================================================================ +You might also leverage this functionality if multiple {kib} instances connected to the same {es} cluster use different encryption keys. In this case, you might have a mix of saved objects encrypted with different keys, and every {kib} instance can only deal with a specific subset of objects. To fix this, you must choose a single primary encryption key for `xpack.encryptedSavedObjects.encryptionKey`, move all other encryption keys to `xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys`, and sync this configuration across all {kib} instances. +============================================================================ + +At some point, you might want to dispose of old encryption keys completely. Make sure there are no saved objects that {kib} encrypted with these encryption keys. You can use the <> to determine which existing saved objects require decryption-only keys and re-encrypt them with the primary key. diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 0f02279eaf1f3..e7bd297a3ebb5 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -129,3 +129,4 @@ include::securing-communications/elasticsearch-mutual-tls.asciidoc[] include::audit-logging.asciidoc[] include::access-agreement.asciidoc[] include::session-management.asciidoc[] +include::secure-saved-objects.asciidoc[] From db4184fe8b71d17fcccd1f9ff42072c2fa739957 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 16 Oct 2020 12:17:21 -0400 Subject: [PATCH 62/79] [Alerting UI] Updating 'Add new' wording (#80509) * Updating 'Add new' wording * PR fixes * i18n_check fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - .../components/builtin_action_types/email/email_params.tsx | 5 ++--- .../sections/action_connector_form/action_form.test.tsx | 2 +- .../sections/action_connector_form/action_form.tsx | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d0c24b0239b62..ba96d5ee9cdb2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18015,7 +18015,6 @@ "xpack.triggersActionsUI.sections.alertsList.unableToLoadActionTypesMessage": "アクションタイプを読み込めません", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage": "アラートを読み込めません", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertTypesMessage": "アラートタイプを読み込めません", - "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "{titleBcc}", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc を追加", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel": "送信元", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel": "ホスト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index db73cd8043e7e..7e5882279b406 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18026,7 +18026,6 @@ "xpack.triggersActionsUI.sections.alertsList.unableToLoadActionTypesMessage": "无法加载操作类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertsMessage": "无法加载告警", "xpack.triggersActionsUI.sections.alertsList.unableToLoadAlertTypesMessage": "无法加载告警类型", - "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "{titleBcc}", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "添加抄送收件人", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel": "发送者", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel": "主机", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index 39c59a10fbc81..a91cf3e7552bc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -52,7 +52,7 @@ export const EmailParamsFields = ({ {!addCC ? ( setAddCC(true)}> @@ -60,9 +60,8 @@ export const EmailParamsFields = ({ {!addBCC ? ( setAddBCC(true)}> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 34569dcc75240..3e229c6a2333d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -366,7 +366,7 @@ describe('action_form', () => { `); }); - it('does not render "Add new" button for preconfigured only action type', async () => { + it('does not render "Add connector" button for preconfigured only action type', async () => { await setup(); const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); actionOption.first().simulate('click'); 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 94571c4eb1e5b..61cf3f2d37925 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 @@ -279,7 +279,7 @@ export const ActionForm = ({ }} > From c90dabaeba6bd967828cc64a43d90148a6c34f48 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Fri, 16 Oct 2020 18:46:51 +0200 Subject: [PATCH 63/79] removing `kibana_datatable` in favor of `datatable` (#80548) --- ...beddable-public.rangeselectcontext.data.md | 2 +- ...ns-embeddable-public.rangeselectcontext.md | 2 +- ...mbeddable-public.valueclickcontext.data.md | 2 +- ...ins-embeddable-public.valueclickcontext.md | 2 +- ...-expressions-public.datatablecolumntype.md | 4 +- ...ressions-public.kibanadatatable.columns.md | 11 - ...gins-expressions-public.kibanadatatable.md | 20 -- ...expressions-public.kibanadatatable.rows.md | 11 - ...expressions-public.kibanadatatable.type.md | 11 - ...public.kibanadatatablecolumn.formathint.md | 11 - ...essions-public.kibanadatatablecolumn.id.md | 11 - ...xpressions-public.kibanadatatablecolumn.md | 21 -- ...sions-public.kibanadatatablecolumn.meta.md | 11 - ...sions-public.kibanadatatablecolumn.name.md | 11 - ...banadatatablecolumnmeta.aggconfigparams.md | 11 - ...ibanadatatablecolumnmeta.indexpatternid.md | 11 - ...ssions-public.kibanadatatablecolumnmeta.md | 20 -- ...s-public.kibanadatatablecolumnmeta.type.md | 11 - ...s-expressions-public.kibanadatatablerow.md | 11 - ...ibana-plugin-plugins-expressions-public.md | 6 +- ...-expressions-server.datatablecolumntype.md | 4 +- ...ressions-server.kibanadatatable.columns.md | 11 - ...gins-expressions-server.kibanadatatable.md | 20 -- ...expressions-server.kibanadatatable.rows.md | 11 - ...expressions-server.kibanadatatable.type.md | 11 - ...server.kibanadatatablecolumn.formathint.md | 11 - ...essions-server.kibanadatatablecolumn.id.md | 11 - ...xpressions-server.kibanadatatablecolumn.md | 21 -- ...sions-server.kibanadatatablecolumn.meta.md | 11 - ...sions-server.kibanadatatablecolumn.name.md | 11 - ...banadatatablecolumnmeta.aggconfigparams.md | 11 - ...ibanadatatablecolumnmeta.indexpatternid.md | 11 - ...ssions-server.kibanadatatablecolumnmeta.md | 20 -- ...s-server.kibanadatatablecolumnmeta.type.md | 11 - ...s-expressions-server.kibanadatatablerow.md | 11 - ...ibana-plugin-plugins-expressions-server.md | 6 +- .../data/common/field_formats/field_format.ts | 5 +- .../data/common/search/expressions/esaggs.ts | 4 +- .../create_filters_from_range_select.test.ts | 53 +++-- .../create_filters_from_range_select.ts | 17 +- .../create_filters_from_value_click.test.ts | 20 +- .../create_filters_from_value_click.ts | 36 ++-- .../data/public/search/expressions/esaggs.ts | 38 ++-- .../data/public/search/expressions/index.ts | 1 - .../public/search/expressions/utils/index.ts | 20 -- .../expressions/utils/serialize_agg_config.ts | 54 ----- .../public/lib/triggers/triggers.ts | 6 +- src/plugins/embeddable/public/public.api.md | 10 +- .../expression_types/specs/datatable.ts | 41 +++- .../common/expression_types/specs/index.ts | 3 - .../specs/kibana_datatable.ts | 75 ------- src/plugins/expressions/public/index.ts | 4 - src/plugins/expressions/public/public.api.md | 52 +---- src/plugins/expressions/server/index.ts | 4 - src/plugins/expressions/server/server.api.md | 52 +---- .../public/input_control_fn.ts | 4 +- .../__snapshots__/region_map_fn.test.js.snap | 2 +- .../region_map/public/region_map_fn.js | 2 +- .../region_map/public/region_map_fn.test.js | 2 +- src/plugins/tile_map/public/tile_map_fn.js | 2 +- .../tile_map/public/tile_map_visualization.js | 16 +- .../tile_map/public/tilemap_fn.test.js | 2 +- .../public/markdown_renderer.tsx | 2 +- .../__snapshots__/metric_vis_fn.test.ts.snap | 2 +- .../components/metric_vis_component.tsx | 4 +- .../public/metric_vis_fn.test.ts | 2 +- .../vis_type_metric/public/metric_vis_fn.ts | 6 +- .../public/metric_vis_renderer.tsx | 6 +- .../public/table_vis_fn.test.ts | 2 +- .../vis_type_table/public/table_vis_fn.ts | 6 +- .../__snapshots__/tag_cloud_fn.test.ts.snap | 2 +- .../public/tag_cloud_fn.test.ts | 2 +- .../vis_type_tagcloud/public/tag_cloud_fn.ts | 8 +- .../public/tag_cloud_vis_renderer.tsx | 2 +- .../public/timelion_vis_renderer.tsx | 2 +- .../vis_type_vislib/public/pie_fn.test.ts | 2 +- src/plugins/vis_type_vislib/public/pie_fn.ts | 6 +- .../public/vis_type_vislib_vis_fn.ts | 6 +- .../components/visualization_container.tsx | 5 +- .../public/expression_functions/range.ts | 4 +- .../expression_functions/vis_dimension.ts | 10 +- .../screenshots/baseline/combined_test.png | Bin 16994 -> 22893 bytes .../baseline/final_screenshot_test.png | Bin 17033 -> 22943 bytes .../screenshots/baseline/metric_all_data.png | Bin 24240 -> 33019 bytes .../baseline/metric_invalid_data.png | Bin 1806 -> 4518 bytes .../baseline/metric_multi_metric_data.png | Bin 46180 -> 46289 bytes .../baseline/metric_percentage_mode.png | Bin 23705 -> 32672 bytes .../baseline/metric_single_metric_data.png | Bin 22004 -> 29766 bytes .../screenshots/baseline/partial_test_1.png | Bin 11228 -> 15123 bytes .../screenshots/baseline/partial_test_2.png | Bin 16994 -> 22893 bytes .../screenshots/baseline/partial_test_3.png | Bin 7054 -> 1994 bytes .../baseline/tagcloud_all_data.png | Bin 11827 -> 15647 bytes .../baseline/tagcloud_fontsize.png | Bin 10314 -> 13748 bytes .../baseline/tagcloud_invalid_data.png | Bin 1806 -> 1994 bytes .../baseline/tagcloud_metric_data.png | Bin 6712 -> 9238 bytes .../screenshots/baseline/tagcloud_options.png | Bin 15100 -> 19125 bytes .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../baseline/metric_invalid_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/partial_test_3.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_invalid_data.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/combined_test2.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../session/metric_invalid_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_1.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/partial_test_3.json | 2 +- .../snapshots/session/step_output_test2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../snapshots/session/tagcloud_all_data.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- .../session/tagcloud_invalid_data.json | 2 +- .../session/tagcloud_metric_data.json | 2 +- .../snapshots/session/tagcloud_options.json | 2 +- .../test_suites/run_pipeline/helpers.ts | 14 +- .../public/components/datatable/datatable.tsx | 6 +- x-pack/plugins/canvas/types/state.ts | 2 - .../public/lib/url_drilldown.test.ts | 18 +- .../public/lib/url_drilldown_scope.test.ts | 11 +- .../public/lib/url_drilldown_scope.ts | 4 +- .../expression.test.tsx | 70 ++++++- .../datatable_visualization/expression.tsx | 13 +- .../editor_frame_service/format_column.ts | 26 ++- .../editor_frame_service/merge_tables.test.ts | 18 +- .../editor_frame_service/merge_tables.ts | 8 +- .../rename_columns.test.ts | 56 +++-- .../indexpattern_datasource/rename_columns.ts | 18 +- .../metric_visualization/expression.test.tsx | 8 +- .../metric_visualization/expression.tsx | 4 +- .../render_function.test.tsx | 19 +- .../pie_visualization/render_function.tsx | 4 +- .../pie_visualization/render_helpers.test.ts | 55 ++--- .../pie_visualization/render_helpers.ts | 6 +- .../lens/public/pie_visualization/types.ts | 6 +- x-pack/plugins/lens/public/types.ts | 4 +- x-pack/plugins/lens/public/utils.test.ts | 8 +- x-pack/plugins/lens/public/utils.ts | 6 +- .../axes_configuration.test.ts | 132 +++++++----- .../xy_visualization/axes_configuration.ts | 9 +- .../xy_visualization/expression.test.tsx | 196 ++++++++++-------- .../public/xy_visualization/expression.tsx | 23 +- 159 files changed, 711 insertions(+), 1101 deletions(-) delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md delete mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md delete mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md delete mode 100644 src/plugins/data/public/search/expressions/utils/index.ts delete mode 100644 src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts delete mode 100644 src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md index 6d2774d86f109..f11003887a6df 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md @@ -8,7 +8,7 @@ ```typescript data: { - table: KibanaDatatable; + table: Datatable; column: number; range: number[]; timeFieldName?: string; diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md index 0f92ed86301da..f23cb44a7f014 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.rangeselectcontext.md @@ -14,6 +14,6 @@ export interface RangeSelectContext | Property | Type | Description | | --- | --- | --- | -| [data](./kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md) | {
table: KibanaDatatable;
column: number;
range: number[];
timeFieldName?: string;
} | | +| [data](./kibana-plugin-plugins-embeddable-public.rangeselectcontext.data.md) | {
table: Datatable;
column: number;
range: number[];
timeFieldName?: string;
} | | | [embeddable](./kibana-plugin-plugins-embeddable-public.rangeselectcontext.embeddable.md) | T | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md index 92c33affc47a9..e7c1be172cd70 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md @@ -9,7 +9,7 @@ ```typescript data: { data: Array<{ - table: Pick; + table: Pick; column: number; row: number; value: any; diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md index 13133095956c6..875c8d276160e 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.valueclickcontext.md @@ -14,6 +14,6 @@ export interface ValueClickContext | Property | Type | Description | | --- | --- | --- | -| [data](./kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md) | {
data: Array<{
table: Pick<KibanaDatatable, 'rows' | 'columns'>;
column: number;
row: number;
value: any;
}>;
timeFieldName?: string;
negate?: boolean;
} | | +| [data](./kibana-plugin-plugins-embeddable-public.valueclickcontext.data.md) | {
data: Array<{
table: Pick<Datatable, 'rows' | 'columns'>;
column: number;
row: number;
value: any;
}>;
timeFieldName?: string;
negate?: boolean;
} | | | [embeddable](./kibana-plugin-plugins-embeddable-public.valueclickcontext.embeddable.md) | T | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md index a06ab351e62c3..8f134bd3bfe95 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.datatablecolumntype.md @@ -4,10 +4,10 @@ ## DatatableColumnType type -This type represents the `type` of any `DatatableColumn` in a `Datatable`. +This type represents the `type` of any `DatatableColumn` in a `Datatable`. its duplicated from KBN\_FIELD\_TYPES Signature: ```typescript -export declare type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export declare type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md deleted file mode 100644 index c8aa768a883d6..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) > [columns](./kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md) - -## KibanaDatatable.columns property - -Signature: - -```typescript -columns: KibanaDatatableColumn[]; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md deleted file mode 100644 index 4ea1d6f42b66d..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) - -## KibanaDatatable interface - -Signature: - -```typescript -export interface KibanaDatatable -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [columns](./kibana-plugin-plugins-expressions-public.kibanadatatable.columns.md) | KibanaDatatableColumn[] | | -| [rows](./kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md) | KibanaDatatableRow[] | | -| [type](./kibana-plugin-plugins-expressions-public.kibanadatatable.type.md) | typeof name | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md deleted file mode 100644 index 43f3243dc4fa7..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) > [rows](./kibana-plugin-plugins-expressions-public.kibanadatatable.rows.md) - -## KibanaDatatable.rows property - -Signature: - -```typescript -rows: KibanaDatatableRow[]; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md deleted file mode 100644 index 996f59cbb77a1..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatable.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) > [type](./kibana-plugin-plugins-expressions-public.kibanadatatable.type.md) - -## KibanaDatatable.type property - -Signature: - -```typescript -type: typeof name; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md deleted file mode 100644 index b517c1610261b..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [formatHint](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md) - -## KibanaDatatableColumn.formatHint property - -Signature: - -```typescript -formatHint?: SerializedFieldFormat; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md deleted file mode 100644 index e7d43190589a7..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [id](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md) - -## KibanaDatatableColumn.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md deleted file mode 100644 index 138c19f0ec7bd..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) - -## KibanaDatatableColumn interface - -Signature: - -```typescript -export interface KibanaDatatableColumn -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [formatHint](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.formathint.md) | SerializedFieldFormat | | -| [id](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.id.md) | string | | -| [meta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md) | KibanaDatatableColumnMeta | | -| [name](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md) | string | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md deleted file mode 100644 index df2d09bf3cc55..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [meta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.meta.md) - -## KibanaDatatableColumn.meta property - -Signature: - -```typescript -meta?: KibanaDatatableColumnMeta; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md deleted file mode 100644 index 841ad67f3f521..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) > [name](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.name.md) - -## KibanaDatatableColumn.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md deleted file mode 100644 index 2ec6edda4cbca..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) > [aggConfigParams](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md) - -## KibanaDatatableColumnMeta.aggConfigParams property - -Signature: - -```typescript -aggConfigParams?: Record; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md deleted file mode 100644 index 2287c28398f7f..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) > [indexPatternId](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md) - -## KibanaDatatableColumnMeta.indexPatternId property - -Signature: - -```typescript -indexPatternId?: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md deleted file mode 100644 index b2f8c9d06a727..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) - -## KibanaDatatableColumnMeta interface - -Signature: - -```typescript -export interface KibanaDatatableColumnMeta -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggConfigParams](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.aggconfigparams.md) | Record<string, any> | | -| [indexPatternId](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.indexpatternid.md) | string | | -| [type](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md) | string | | - diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md deleted file mode 100644 index 98d4a0c2d43c3..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) > [type](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.type.md) - -## KibanaDatatableColumnMeta.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md deleted file mode 100644 index cb5f1ad70f628..0000000000000 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.kibanadatatablerow.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [KibanaDatatableRow](./kibana-plugin-plugins-expressions-public.kibanadatatablerow.md) - -## KibanaDatatableRow interface - -Signature: - -```typescript -export interface KibanaDatatableRow -``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index b0c732188a46e..db09f966e2fa5 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -72,10 +72,6 @@ | [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) | | | [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) | | | [IRegistry](./kibana-plugin-plugins-expressions-public.iregistry.md) | | -| [KibanaDatatable](./kibana-plugin-plugins-expressions-public.kibanadatatable.md) | | -| [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumn.md) | | -| [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-public.kibanadatatablecolumnmeta.md) | | -| [KibanaDatatableRow](./kibana-plugin-plugins-expressions-public.kibanadatatablerow.md) | | | [PointSeriesColumn](./kibana-plugin-plugins-expressions-public.pointseriescolumn.md) | Column in a PointSeries | | [Range](./kibana-plugin-plugins-expressions-public.range.md) | | | [ReactExpressionRendererProps](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) | | @@ -95,7 +91,7 @@ | [AnyExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-public.anyexpressionfunctiondefinition.md) | Type to capture every possible expression function definition. | | [AnyExpressionTypeDefinition](./kibana-plugin-plugins-expressions-public.anyexpressiontypedefinition.md) | | | [ArgumentType](./kibana-plugin-plugins-expressions-public.argumenttype.md) | This type represents all of the possible combinations of properties of an Argument in an Expression Function. The presence or absence of certain fields influence the shape and presence of others within each arg in the specification. | -| [DatatableColumnType](./kibana-plugin-plugins-expressions-public.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. | +| [DatatableColumnType](./kibana-plugin-plugins-expressions-public.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. its duplicated from KBN\_FIELD\_TYPES | | [DatatableRow](./kibana-plugin-plugins-expressions-public.datatablerow.md) | This type represents a row in a Datatable. | | [ExecutionContainer](./kibana-plugin-plugins-expressions-public.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-public.executorcontainer.md) | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md index 4afce913526de..dc98acffa1236 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.datatablecolumntype.md @@ -4,10 +4,10 @@ ## DatatableColumnType type -This type represents the `type` of any `DatatableColumn` in a `Datatable`. +This type represents the `type` of any `DatatableColumn` in a `Datatable`. its duplicated from KBN\_FIELD\_TYPES Signature: ```typescript -export declare type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export declare type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md deleted file mode 100644 index 423e543e4307a..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) > [columns](./kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md) - -## KibanaDatatable.columns property - -Signature: - -```typescript -columns: KibanaDatatableColumn[]; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md deleted file mode 100644 index 30ee3ac2fcd13..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) - -## KibanaDatatable interface - -Signature: - -```typescript -export interface KibanaDatatable -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [columns](./kibana-plugin-plugins-expressions-server.kibanadatatable.columns.md) | KibanaDatatableColumn[] | | -| [rows](./kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md) | KibanaDatatableRow[] | | -| [type](./kibana-plugin-plugins-expressions-server.kibanadatatable.type.md) | typeof name | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md deleted file mode 100644 index 42170a83fc3c8..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) > [rows](./kibana-plugin-plugins-expressions-server.kibanadatatable.rows.md) - -## KibanaDatatable.rows property - -Signature: - -```typescript -rows: KibanaDatatableRow[]; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md deleted file mode 100644 index c36674540a1ba..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatable.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) > [type](./kibana-plugin-plugins-expressions-server.kibanadatatable.type.md) - -## KibanaDatatable.type property - -Signature: - -```typescript -type: typeof name; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md deleted file mode 100644 index a1e6949019dcb..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [formatHint](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md) - -## KibanaDatatableColumn.formatHint property - -Signature: - -```typescript -formatHint?: SerializedFieldFormat; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md deleted file mode 100644 index 6f90da1ac9c94..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [id](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md) - -## KibanaDatatableColumn.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md deleted file mode 100644 index 171477911502f..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) - -## KibanaDatatableColumn interface - -Signature: - -```typescript -export interface KibanaDatatableColumn -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [formatHint](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.formathint.md) | SerializedFieldFormat | | -| [id](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.id.md) | string | | -| [meta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md) | KibanaDatatableColumnMeta | | -| [name](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md) | string | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md deleted file mode 100644 index 40b20d51e6ec6..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [meta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.meta.md) - -## KibanaDatatableColumn.meta property - -Signature: - -```typescript -meta?: KibanaDatatableColumnMeta; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md deleted file mode 100644 index 3a85e2325483a..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) > [name](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.name.md) - -## KibanaDatatableColumn.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md deleted file mode 100644 index 539b24174f725..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) > [aggConfigParams](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md) - -## KibanaDatatableColumnMeta.aggConfigParams property - -Signature: - -```typescript -aggConfigParams?: Record; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md deleted file mode 100644 index 2704915a15071..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) > [indexPatternId](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md) - -## KibanaDatatableColumnMeta.indexPatternId property - -Signature: - -```typescript -indexPatternId?: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md deleted file mode 100644 index d9a96e665f010..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) - -## KibanaDatatableColumnMeta interface - -Signature: - -```typescript -export interface KibanaDatatableColumnMeta -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggConfigParams](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.aggconfigparams.md) | Record<string, any> | | -| [indexPatternId](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.indexpatternid.md) | string | | -| [type](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md) | string | | - diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md deleted file mode 100644 index 56e3757ef621a..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) > [type](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.type.md) - -## KibanaDatatableColumnMeta.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md deleted file mode 100644 index dd0f3f4cb2f60..0000000000000 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.kibanadatatablerow.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [KibanaDatatableRow](./kibana-plugin-plugins-expressions-server.kibanadatatablerow.md) - -## KibanaDatatableRow interface - -Signature: - -```typescript -export interface KibanaDatatableRow -``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md index dd7c7af466bd0..9e2189dad2732 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.md @@ -63,10 +63,6 @@ | [Font](./kibana-plugin-plugins-expressions-server.font.md) | An interface representing a font in Canvas, with a textual label and the CSS font-value. | | [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) | | | [IRegistry](./kibana-plugin-plugins-expressions-server.iregistry.md) | | -| [KibanaDatatable](./kibana-plugin-plugins-expressions-server.kibanadatatable.md) | | -| [KibanaDatatableColumn](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumn.md) | | -| [KibanaDatatableColumnMeta](./kibana-plugin-plugins-expressions-server.kibanadatatablecolumnmeta.md) | | -| [KibanaDatatableRow](./kibana-plugin-plugins-expressions-server.kibanadatatablerow.md) | | | [PointSeriesColumn](./kibana-plugin-plugins-expressions-server.pointseriescolumn.md) | Column in a PointSeries | | [Range](./kibana-plugin-plugins-expressions-server.range.md) | | | [SerializedDatatable](./kibana-plugin-plugins-expressions-server.serializeddatatable.md) | | @@ -79,7 +75,7 @@ | [AnyExpressionFunctionDefinition](./kibana-plugin-plugins-expressions-server.anyexpressionfunctiondefinition.md) | Type to capture every possible expression function definition. | | [AnyExpressionTypeDefinition](./kibana-plugin-plugins-expressions-server.anyexpressiontypedefinition.md) | | | [ArgumentType](./kibana-plugin-plugins-expressions-server.argumenttype.md) | This type represents all of the possible combinations of properties of an Argument in an Expression Function. The presence or absence of certain fields influence the shape and presence of others within each arg in the specification. | -| [DatatableColumnType](./kibana-plugin-plugins-expressions-server.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. | +| [DatatableColumnType](./kibana-plugin-plugins-expressions-server.datatablecolumntype.md) | This type represents the type of any DatatableColumn in a Datatable. its duplicated from KBN\_FIELD\_TYPES | | [DatatableRow](./kibana-plugin-plugins-expressions-server.datatablerow.md) | This type represents a row in a Datatable. | | [ExecutionContainer](./kibana-plugin-plugins-expressions-server.executioncontainer.md) | | | [ExecutorContainer](./kibana-plugin-plugins-expressions-server.executorcontainer.md) | | diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 9e4308d6fd559..15ecf6e4fc3ef 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -185,7 +185,8 @@ export abstract class FieldFormat { const params = transform( this._params, - (uniqParams: any, val, param) => { + (uniqParams: any, val, param: string) => { + if (param === 'parsedUrl') return; if (param && val !== get(defaultsParams, param)) { uniqParams[param] = val; } @@ -195,7 +196,7 @@ export abstract class FieldFormat { return { id, - params: size(params) ? params : undefined, + params: size(params) ? (params as any) : undefined, }; } diff --git a/src/plugins/data/common/search/expressions/esaggs.ts b/src/plugins/data/common/search/expressions/esaggs.ts index 2957512886b4d..4f65babdcd360 100644 --- a/src/plugins/data/common/search/expressions/esaggs.ts +++ b/src/plugins/data/common/search/expressions/esaggs.ts @@ -19,12 +19,12 @@ import { KibanaContext, - KibanaDatatable, + Datatable, ExpressionFunctionDefinition, } from '../../../../../plugins/expressions/common'; type Input = KibanaContext | null; -type Output = Promise; +type Output = Promise; interface Arguments { index: string; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts index aa96d77d873d3..6dcfa4d02bcb2 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -21,7 +21,12 @@ import moment from 'moment'; import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select'; -import { IndexPatternsContract, RangeFilter } from '../../../public'; +import { + fieldFormats, + FieldFormatsGetConfigFn, + IndexPatternsContract, + RangeFilter, +} from '../../../public'; import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns, setSearchService } from '../../../public/services'; import { TriggerContextMapping } from '../../../../ui_actions/public'; @@ -31,23 +36,30 @@ describe('brushEvent', () => { const JAN_01_2014 = 1388559600000; let baseEvent: TriggerContextMapping['SELECT_RANGE_TRIGGER']['data']; + const mockField = { + name: 'time', + indexPattern: { + id: 'logstash-*', + }, + filterable: true, + format: new fieldFormats.DateFormat({}, (() => {}) as FieldFormatsGetConfigFn), + }; + const indexPattern = { id: 'indexPatternId', timeFieldName: 'time', fields: { - getByName: () => undefined, - filter: () => [], + getByName: () => mockField, + filter: () => [mockField], }, }; - const aggConfigs = [ - { - params: { - field: {}, - }, - getIndexPattern: () => indexPattern, + const serializedAggConfig = { + type: 'date_histogram', + params: { + field: {}, }, - ]; + }; beforeEach(() => { const dataStart = dataPluginMock.createStartContract(); @@ -60,15 +72,18 @@ describe('brushEvent', () => { baseEvent = { column: 0, table: { - type: 'kibana_datatable', + type: 'datatable', columns: [ { id: '1', name: '1', meta: { - type: 'histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: aggConfigs[0].params, + type: 'date', + sourceParams: { + indexPatternId: 'indexPatternId', + ...serializedAggConfig, + }, + source: 'esaggs', }, }, ], @@ -90,7 +105,7 @@ describe('brushEvent', () => { describe('handles an event when the x-axis field is a date field', () => { describe('date field is index pattern timefield', () => { beforeEach(() => { - aggConfigs[0].params.field = { + serializedAggConfig.params.field = { name: 'time', type: 'date', }; @@ -98,7 +113,7 @@ describe('brushEvent', () => { afterAll(() => { baseEvent.range = []; - aggConfigs[0].params.field = {}; + serializedAggConfig.params.field = {}; }); test('by ignoring the event when range spans zero time', async () => { @@ -123,7 +138,7 @@ describe('brushEvent', () => { describe('date field is not index pattern timefield', () => { beforeEach(() => { - aggConfigs[0].params.field = { + serializedAggConfig.params.field = { name: 'anotherTimeField', type: 'date', }; @@ -131,7 +146,7 @@ describe('brushEvent', () => { afterAll(() => { baseEvent.range = []; - aggConfigs[0].params.field = {}; + serializedAggConfig.params.field = {}; }); test('creates a new range filter', async () => { @@ -157,7 +172,7 @@ describe('brushEvent', () => { describe('handles an event when the x-axis field is a number', () => { beforeAll(() => { - aggConfigs[0].params.field = { + serializedAggConfig.params.field = { name: 'numberField', type: 'number', }; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts index d9aa1b8ec8048..2d7aeff79a689 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -20,9 +20,9 @@ import { last } from 'lodash'; import moment from 'moment'; import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; -import { deserializeAggConfig } from '../../search/expressions/utils'; -import type { RangeSelectContext } from '../../../../embeddable/public'; +import { getIndexPatterns, getSearchService } from '../../../public/services'; +import { RangeSelectContext } from '../../../../embeddable/public'; +import { AggConfigSerialized } from '../../../common/search/aggs'; export async function createFiltersFromRangeSelectAction(event: RangeSelectContext['data']) { const column: Record = event.table.columns[event.column]; @@ -31,11 +31,12 @@ export async function createFiltersFromRangeSelectAction(event: RangeSelectConte return []; } - const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); - const aggConfig = deserializeAggConfig({ - ...column.meta, - indexPattern, - }); + const { indexPatternId, ...aggConfigs } = column.meta.sourceParams; + const indexPattern = await getIndexPatterns().get(indexPatternId); + const aggConfigsInstance = getSearchService().aggs.createAggConfigs(indexPattern, [ + aggConfigs as AggConfigSerialized, + ]); + const aggConfig = aggConfigsInstance.aggs[0]; const field: IFieldType = aggConfig.params.field; if (!field || event.range.length <= 1) { diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 2ad20c3807819..23d2ab080d75e 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -45,12 +45,16 @@ describe('createFiltersFromValueClick', () => { name: 'test', id: '1-1', meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field: 'bytes', - interval: 30, - otherBucket: true, + type: 'date', + source: 'esaggs', + sourceParams: { + indexPatternId: 'logstash-*', + type: 'histogram', + params: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, }, }, }, @@ -91,9 +95,7 @@ describe('createFiltersFromValueClick', () => { }); test('handles an event when aggregations type is a terms', async () => { - if (dataPoints[0].table.columns[0].meta) { - dataPoints[0].table.columns[0].meta.type = 'terms'; - } + (dataPoints[0].table.columns[0].meta.sourceParams as any).type = 'terms'; const filters = await createFiltersFromValueClickAction({ data: dataPoints }); expect(filters.length).toEqual(1); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index 9429df91f693c..ce7ecf434056a 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -17,11 +17,11 @@ * under the License. */ -import { KibanaDatatable } from '../../../../../plugins/expressions/public'; -import { deserializeAggConfig } from '../../search/expressions'; +import { Datatable } from '../../../../../plugins/expressions/public'; import { esFilters, Filter } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; -import type { ValueClickContext } from '../../../../embeddable/public'; +import { getIndexPatterns, getSearchService } from '../../../public/services'; +import { ValueClickContext } from '../../../../embeddable/public'; +import { AggConfigSerialized } from '../../../common/search/aggs'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -33,7 +33,7 @@ import type { ValueClickContext } from '../../../../embeddable/public'; * @return {array} - array of terms to filter against */ const getOtherBucketFilterTerms = ( - table: Pick, + table: Pick, columnIndex: number, rowIndex: number ) => { @@ -71,22 +71,28 @@ const getOtherBucketFilterTerms = ( * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() */ const createFilter = async ( - table: Pick, + table: Pick, columnIndex: number, rowIndex: number ) => { - if (!table || !table.columns || !table.columns[columnIndex]) { + if ( + !table || + !table.columns || + !table.columns[columnIndex] || + !table.columns[columnIndex].meta || + table.columns[columnIndex].meta.source !== 'esaggs' || + !table.columns[columnIndex].meta.sourceParams?.indexPatternId + ) { return; } const column = table.columns[columnIndex]; - if (!column.meta || !column.meta.indexPatternId) { - return; - } - const aggConfig = deserializeAggConfig({ - type: column.meta.type, - aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, - indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), - }); + const { indexPatternId, ...aggConfigParams } = table.columns[columnIndex].meta + .sourceParams as any; + const aggConfigsInstance = getSearchService().aggs.createAggConfigs( + await getIndexPatterns().get(indexPatternId), + [aggConfigParams as AggConfigSerialized] + ); + const aggConfig = aggConfigsInstance.aggs[0]; let filter: Filter[] = []; const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 1f72bda44e4ed..de7a4ffce8bd4 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -19,7 +19,7 @@ import { get, hasIn } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; +import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -49,7 +49,6 @@ import { getSearchService, } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; -import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -195,11 +194,9 @@ const handleCourierRequest = async ({ : undefined, }; - (searchSource as any).tabifiedResponse = tabifyAggResponse( - aggs, - (searchSource as any).finalResponse, - tabifyParams - ); + const response = tabifyAggResponse(aggs, (searchSource as any).finalResponse, tabifyParams); + + (searchSource as any).tabifiedResponse = response; inspectorAdapters.data.setTabularLoader( () => @@ -210,12 +207,12 @@ const handleCourierRequest = async ({ { returnsFormattedValues: true } ); - return (searchSource as any).tabifiedResponse; + return response; }; export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ name, - type: 'kibana_datatable', + type: 'datatable', inputTypes: ['kibana_context', 'null'], help: i18n.translate('data.functions.esaggs.help', { defaultMessage: 'Run AggConfig aggregation', @@ -281,18 +278,25 @@ export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ abortSignal: (abortSignal as unknown) as AbortSignal, }); - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', rows: response.rows, - columns: response.columns.map((column: any) => { - const cleanedColumn: KibanaDatatableColumn = { + columns: response.columns.map((column) => { + const cleanedColumn: DatatableColumn = { id: column.id, name: column.name, - meta: serializeAggConfig(column.aggConfig), + meta: { + type: column.aggConfig.params.field?.type || 'number', + field: column.aggConfig.params.field?.name, + index: indexPattern.title, + params: column.aggConfig.toSerializedFieldFormat(), + source: 'esaggs', + sourceParams: { + indexPatternId: indexPattern.id, + ...column.aggConfig.serialize(), + }, + }, }; - if (args.includeFormatHints) { - cleanedColumn.formatHint = column.aggConfig.toSerializedFieldFormat(); - } return cleanedColumn; }), }; diff --git a/src/plugins/data/public/search/expressions/index.ts b/src/plugins/data/public/search/expressions/index.ts index 02df7986479ad..98ed1d08af8ad 100644 --- a/src/plugins/data/public/search/expressions/index.ts +++ b/src/plugins/data/public/search/expressions/index.ts @@ -20,4 +20,3 @@ export * from './esaggs'; export * from './es_raw_response'; export * from './esdsl'; -export * from './utils'; diff --git a/src/plugins/data/public/search/expressions/utils/index.ts b/src/plugins/data/public/search/expressions/utils/index.ts deleted file mode 100644 index 094536fc18437..0000000000000 --- a/src/plugins/data/public/search/expressions/utils/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './serialize_agg_config'; diff --git a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts deleted file mode 100644 index 6ba323b65783f..0000000000000 --- a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { KibanaDatatableColumnMeta } from '../../../../../../plugins/expressions/public'; -import { IAggConfig } from '../../../../common'; -import { IndexPattern } from '../../../index_patterns'; -import { getSearchService } from '../../../../public/services'; - -/** @internal */ -export const serializeAggConfig = (aggConfig: IAggConfig): KibanaDatatableColumnMeta => { - return { - type: aggConfig.type.name, - indexPatternId: aggConfig.getIndexPattern().id, - aggConfigParams: aggConfig.serialize().params, - }; -}; - -interface DeserializeAggConfigParams { - type: string; - aggConfigParams: Record; - indexPattern: IndexPattern; -} - -/** @internal */ -export const deserializeAggConfig = ({ - type, - aggConfigParams, - indexPattern, -}: DeserializeAggConfigParams) => { - const { aggs } = getSearchService(); - const aggConfigs = aggs.createAggConfigs(indexPattern); - const aggConfig = aggConfigs.createAggConfig({ - enabled: true, - type, - params: aggConfigParams, - }); - return aggConfig; -}; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index ccba5cf771088..54c7a2ecc129d 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -17,7 +17,7 @@ * under the License. */ -import { KibanaDatatable } from '../../../../expressions'; +import { Datatable } from '../../../../expressions'; import { Trigger } from '../../../../ui_actions/public'; import { IEmbeddable } from '..'; @@ -29,7 +29,7 @@ export interface ValueClickContext { embeddable?: T; data: { data: Array<{ - table: Pick; + table: Pick; column: number; row: number; value: any; @@ -42,7 +42,7 @@ export interface ValueClickContext { export interface RangeSelectContext { embeddable?: T; data: { - table: KibanaDatatable; + table: Datatable; column: number; range: number[]; timeFieldName?: string; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 6280d3a2e4a50..a6d90f2766c18 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -326,7 +326,7 @@ export abstract class Embeddable { +export class EmbeddableChildPanel extends React.Component { constructor(props: EmbeddableChildPanelProps); // (undocumented) [panel: string]: any; @@ -477,7 +477,7 @@ export interface EmbeddablePackageState { // Warning: (ae-missing-release-tag) "EmbeddablePanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class EmbeddablePanel extends React.Component { +export class EmbeddablePanel extends React.Component { constructor(props: Props); // (undocumented) closeMyContextMenuPanel: () => void; @@ -810,7 +810,7 @@ export interface PropertySpec { export interface RangeSelectContext { // (undocumented) data: { - table: KibanaDatatable; + table: Datatable; column: number; range: number[]; timeFieldName?: string; @@ -841,7 +841,7 @@ export interface ValueClickContext { // (undocumented) data: { data: Array<{ - table: Pick; + table: Pick; column: number; row: number; value: any; @@ -881,7 +881,7 @@ export const withEmbeddableSubscription: /** * This type represents the `type` of any `DatatableColumn` in a `Datatable`. + * its duplicated from KBN_FIELD_TYPES */ -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export type DatatableColumnType = + | '_source' + | 'attachment' + | 'boolean' + | 'date' + | 'geo_point' + | 'geo_shape' + | 'ip' + | 'murmur3' + | 'number' + | 'string' + | 'unknown' + | 'conflict' + | 'object' + | 'nested' + | 'histogram' + | 'null'; /** * This type represents a row in a `Datatable`. */ export type DatatableRow = Record; +/** + * Datatable column meta information + */ export interface DatatableColumnMeta { type: DatatableColumnType; + /** + * field this column is based on + */ field?: string; + /** + * index/table this column is based on + */ index?: string; - params?: SerializableState; + /** + * serialized field format + */ + params?: SerializedFieldFormat; + /** + * source function that produced this column + */ source?: string; + /** + * any extra parameters for the source that produced this column + */ sourceParams?: SerializableState; } + /** * This type represents the shape of a column in a `Datatable`. */ diff --git a/src/plugins/expressions/common/expression_types/specs/index.ts b/src/plugins/expressions/common/expression_types/specs/index.ts index 31210b11f6b7a..00c52a2545cd6 100644 --- a/src/plugins/expressions/common/expression_types/specs/index.ts +++ b/src/plugins/expressions/common/expression_types/specs/index.ts @@ -23,7 +23,6 @@ import { error } from './error'; import { filter } from './filter'; import { image } from './image'; import { kibanaContext } from './kibana_context'; -import { kibanaDatatable } from './kibana_datatable'; import { nullType } from './null'; import { num } from './num'; import { number } from './number'; @@ -42,7 +41,6 @@ export const typeSpecs: AnyExpressionTypeDefinition[] = [ filter, image, kibanaContext, - kibanaDatatable, nullType, num, number, @@ -60,7 +58,6 @@ export * from './error'; export * from './filter'; export * from './image'; export * from './kibana_context'; -export * from './kibana_datatable'; export * from './null'; export * from './num'; export * from './number'; diff --git a/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts deleted file mode 100644 index e226f3b124eed..0000000000000 --- a/src/plugins/expressions/common/expression_types/specs/kibana_datatable.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map } from 'lodash'; -import { SerializedFieldFormat } from '../../types/common'; -import { Datatable, PointSeries, PointSeriesColumn } from '.'; - -const name = 'kibana_datatable'; - -export interface KibanaDatatableColumnMeta { - type: string; - indexPatternId?: string; - aggConfigParams?: Record; -} - -export interface KibanaDatatableColumn { - id: string; - name: string; - meta?: KibanaDatatableColumnMeta; - formatHint?: SerializedFieldFormat; -} - -export interface KibanaDatatableRow { - [key: string]: unknown; -} - -export interface KibanaDatatable { - type: typeof name; - columns: KibanaDatatableColumn[]; - rows: KibanaDatatableRow[]; -} - -export const kibanaDatatable = { - name, - from: { - datatable: (context: Datatable) => { - return { - type: name, - rows: context.rows, - columns: context.columns.map((column) => { - return { - id: column.name, - name: column.name, - }; - }), - }; - }, - pointseries: (context: PointSeries) => { - const columns = map(context.columns, (column: PointSeriesColumn, n) => { - return { id: n, name: n, ...column }; - }); - return { - type: name, - rows: context.rows, - columns, - }; - }, - }, -}; diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 039890c9233cf..893d68238747d 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -98,10 +98,6 @@ export { isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, - KibanaDatatable, - KibanaDatatableColumn, - KibanaDatatableColumnMeta, - KibanaDatatableRow, KnownTypeToString, Overflow, parse, diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 3f74fe045dd2c..95ee651d433ac 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -82,7 +82,7 @@ export interface DatatableColumn { // Warning: (ae-missing-release-tag) "DatatableColumnType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; // Warning: (ae-missing-release-tag) "DatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -944,54 +944,6 @@ export type KIBANA_CONTEXT_NAME = 'kibana_context'; // @public (undocumented) export type KibanaContext = ExpressionValueSearchContext; -// Warning: (ae-missing-release-tag) "KibanaDatatable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatable { - // (undocumented) - columns: KibanaDatatableColumn[]; - // (undocumented) - rows: KibanaDatatableRow[]; - // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts - // - // (undocumented) - type: typeof name_3; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumn { - // (undocumented) - formatHint?: SerializedFieldFormat; - // (undocumented) - id: string; - // (undocumented) - meta?: KibanaDatatableColumnMeta; - // (undocumented) - name: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumnMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumnMeta { - // (undocumented) - aggConfigParams?: Record; - // (undocumented) - indexPatternId?: string; - // (undocumented) - type: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableRow { - // (undocumented) - [key: string]: unknown; -} - // Warning: (ae-missing-release-tag) "KnownTypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -1077,7 +1029,7 @@ export interface Range { // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // // (undocumented) - type: typeof name_4; + type: typeof name_3; } // Warning: (ae-missing-release-tag) "ReactExpressionRenderer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index 6785457321595..cc22d4b500d97 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -89,10 +89,6 @@ export { isExpressionAstBuilder, KIBANA_CONTEXT_NAME, KibanaContext, - KibanaDatatable, - KibanaDatatableColumn, - KibanaDatatableColumnMeta, - KibanaDatatableRow, KnownTypeToString, Overflow, parse, diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 0c9e4af84f6a5..d5da60af8f8e5 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -79,7 +79,7 @@ export interface DatatableColumn { // Warning: (ae-missing-release-tag) "DatatableColumnType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'null'; +export type DatatableColumnType = '_source' | 'attachment' | 'boolean' | 'date' | 'geo_point' | 'geo_shape' | 'ip' | 'murmur3' | 'number' | 'string' | 'unknown' | 'conflict' | 'object' | 'nested' | 'histogram' | 'null'; // Warning: (ae-missing-release-tag) "DatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -768,54 +768,6 @@ export type KIBANA_CONTEXT_NAME = 'kibana_context'; // @public (undocumented) export type KibanaContext = ExpressionValueSearchContext; -// Warning: (ae-missing-release-tag) "KibanaDatatable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatable { - // (undocumented) - columns: KibanaDatatableColumn[]; - // (undocumented) - rows: KibanaDatatableRow[]; - // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts - // - // (undocumented) - type: typeof name_3; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumn { - // (undocumented) - formatHint?: SerializedFieldFormat; - // (undocumented) - id: string; - // (undocumented) - meta?: KibanaDatatableColumnMeta; - // (undocumented) - name: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableColumnMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableColumnMeta { - // (undocumented) - aggConfigParams?: Record; - // (undocumented) - indexPatternId?: string; - // (undocumented) - type: string; -} - -// Warning: (ae-missing-release-tag) "KibanaDatatableRow" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface KibanaDatatableRow { - // (undocumented) - [key: string]: unknown; -} - // Warning: (ae-missing-release-tag) "KnownTypeToString" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -900,7 +852,7 @@ export interface Range { // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // // (undocumented) - type: typeof name_4; + type: typeof name_3; } // Warning: (ae-missing-release-tag) "SerializedDatatable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts index 59c0e03505bb7..1664555b916b6 100644 --- a/src/plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; interface Arguments { visConfig: string; @@ -34,7 +34,7 @@ interface RenderValue { export const createInputControlVisFn = (): ExpressionFunctionDefinition< 'input_control_vis', - KibanaDatatable, + Datatable, Arguments, Render > => ({ diff --git a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap index 2d615a105906c..cb12712ae824f 100644 --- a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap +++ b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap @@ -50,7 +50,7 @@ Object { "col-0-1": 0, }, ], - "type": "kibana_datatable", + "type": "datatable", }, "visType": "region_map", }, diff --git a/src/plugins/region_map/public/region_map_fn.js b/src/plugins/region_map/public/region_map_fn.js index 314def1fbfdca..fdb7c273720fa 100644 --- a/src/plugins/region_map/public/region_map_fn.js +++ b/src/plugins/region_map/public/region_map_fn.js @@ -23,7 +23,7 @@ export const createRegionMapFn = () => ({ name: 'regionmap', type: 'render', context: { - types: ['kibana_datatable'], + types: ['datatable'], }, help: i18n.translate('regionMap.function.help', { defaultMessage: 'Regionmap visualization', diff --git a/src/plugins/region_map/public/region_map_fn.test.js b/src/plugins/region_map/public/region_map_fn.test.js index 684cc5e897df4..32467541dee02 100644 --- a/src/plugins/region_map/public/region_map_fn.test.js +++ b/src/plugins/region_map/public/region_map_fn.test.js @@ -24,7 +24,7 @@ import { createRegionMapFn } from './region_map_fn'; describe('interpreter/functions#regionmap', () => { const fn = functionWrapper(createRegionMapFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/tile_map/public/tile_map_fn.js b/src/plugins/tile_map/public/tile_map_fn.js index 5f43077bcb24b..3253598d98d94 100644 --- a/src/plugins/tile_map/public/tile_map_fn.js +++ b/src/plugins/tile_map/public/tile_map_fn.js @@ -23,7 +23,7 @@ export const createTileMapFn = () => ({ name: 'tilemap', type: 'render', context: { - types: ['kibana_datatable'], + types: ['datatable'], }, help: i18n.translate('tileMap.function.help', { defaultMessage: 'Tilemap visualization', diff --git a/src/plugins/tile_map/public/tile_map_visualization.js b/src/plugins/tile_map/public/tile_map_visualization.js index b09a2f3bac48f..80084be283658 100644 --- a/src/plugins/tile_map/public/tile_map_visualization.js +++ b/src/plugins/tile_map/public/tile_map_visualization.js @@ -73,19 +73,19 @@ export const createTileMapVisualization = (dependencies) => { }; const bounds = this._kibanaMap.getBounds(); const mapCollar = scaleBounds(bounds); - if (!geoContains(geohashAgg.aggConfigParams.boundingBox, mapCollar)) { + if (!geoContains(geohashAgg.sourceParams.params.boundingBox, mapCollar)) { updateVarsObject.data.boundingBox = { top_left: mapCollar.top_left, bottom_right: mapCollar.bottom_right, }; } else { - updateVarsObject.data.boundingBox = geohashAgg.aggConfigParams.boundingBox; + updateVarsObject.data.boundingBox = geohashAgg.sourceParams.params.boundingBox; } // todo: autoPrecision should be vis parameter, not aggConfig one const zoomPrecision = getZoomPrecision(); - updateVarsObject.data.precision = geohashAgg.aggConfigParams.autoPrecision + updateVarsObject.data.precision = geohashAgg.sourceParams.params.autoPrecision ? zoomPrecision[this.vis.getUiState().get('mapZoom')] - : getPrecision(geohashAgg.aggConfigParams.precision); + : getPrecision(geohashAgg.sourceParams.params.precision); this.vis.eventsSubject.next(updateVarsObject); }; @@ -118,8 +118,8 @@ export const createTileMapVisualization = (dependencies) => { return; } const isAutoPrecision = - typeof geohashAgg.aggConfigParams.autoPrecision === 'boolean' - ? geohashAgg.aggConfigParams.autoPrecision + typeof geohashAgg.sourceParams.params.autoPrecision === 'boolean' + ? geohashAgg.sourceParams.params.autoPrecision : true; if (!isAutoPrecision) { return; @@ -243,7 +243,7 @@ export const createTileMapVisualization = (dependencies) => { } const indexPatternName = agg.indexPatternId; - const field = agg.aggConfigParams.field; + const field = agg.field; const filter = { meta: { negate: false, index: indexPatternName } }; filter[filterName] = { ignore_unmapped: true }; filter[filterName][field] = filterData; @@ -264,7 +264,7 @@ export const createTileMapVisualization = (dependencies) => { const DEFAULT = false; const agg = this._getGeoHashAgg(); if (agg) { - return get(agg, 'aggConfigParams.isFilteredByCollar', DEFAULT); + return get(agg, 'sourceParams.params.isFilteredByCollar', DEFAULT); } else { return DEFAULT; } diff --git a/src/plugins/tile_map/public/tilemap_fn.test.js b/src/plugins/tile_map/public/tilemap_fn.test.js index 8fa12c9f9dbbe..df9fc10a7303c 100644 --- a/src/plugins/tile_map/public/tilemap_fn.test.js +++ b/src/plugins/tile_map/public/tilemap_fn.test.js @@ -41,7 +41,7 @@ import { convertToGeoJson } from '../../maps_legacy/public'; describe('interpreter/functions#tilemap', () => { const fn = functionWrapper(createTileMapFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_markdown/public/markdown_renderer.tsx b/src/plugins/vis_type_markdown/public/markdown_renderer.tsx index 8071196c6a213..f36ffadff7c56 100644 --- a/src/plugins/vis_type_markdown/public/markdown_renderer.tsx +++ b/src/plugins/vis_type_markdown/public/markdown_renderer.tsx @@ -36,7 +36,7 @@ export const markdownVisRenderer: ExpressionRenderDefinition + , domNode diff --git a/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap b/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap index 706d2a902aa90..fd8f3a712d8ae 100644 --- a/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap +++ b/src/plugins/vis_type_metric/public/__snapshots__/metric_vis_fn.test.ts.snap @@ -43,7 +43,7 @@ Object { "col-0-1": 0, }, ], - "type": "kibana_datatable", + "type": "datatable", }, "visType": "metric", }, diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx index e5c7db65c09a8..5ab3ee6eed8eb 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_component.tsx @@ -23,7 +23,7 @@ import { isColorDark } from '@elastic/eui'; import { MetricVisValue } from './metric_vis_value'; import { Input } from '../metric_vis_fn'; import { FieldFormatsContentType, IFieldFormat } from '../../../data/public'; -import { KibanaDatatable } from '../../../expressions/public'; +import { Datatable } from '../../../expressions/public'; import { getHeatmapColors } from '../../../charts/public'; import { VisParams, MetricVisMetric } from '../types'; import { getFormatService } from '../services'; @@ -109,7 +109,7 @@ class MetricVisComponent extends Component { return fieldFormatter.convert(value, format); }; - private processTableGroups(table: KibanaDatatable) { + private processTableGroups(table: Datatable) { const config = this.props.visParams.metric; const dimensions = this.props.visParams.dimensions; const isPercentageMode = config.percentageMode; diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts index 3ed8f8f79a83f..8faa3d2aab265 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -23,7 +23,7 @@ import { functionWrapper } from '../../expressions/common/expression_functions/s describe('interpreter/functions#metric', () => { const fn = functionWrapper(createMetricVisFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts index 97b1e6822333e..20de22f50e63a 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, - KibanaDatatable, + Datatable, Range, Render, Style, @@ -29,7 +29,7 @@ import { import { visType, DimensionsVisParam, VisParams } from './types'; import { ColorSchemas, vislibColorMaps, ColorModes } from '../../charts/public'; -export type Input = KibanaDatatable; +export type Input = Datatable; interface Arguments { percentageMode: boolean; @@ -63,7 +63,7 @@ export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({ name: 'metricVis', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeMetric.function.help', { defaultMessage: 'Metric visualization', }), diff --git a/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx b/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx index bf0d6da9fba05..8e0cb35ca52aa 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx +++ b/src/plugins/vis_type_metric/public/metric_vis_renderer.tsx @@ -36,7 +36,11 @@ export const metricVisRenderer: () => ExpressionRenderDefinition + ({ describe('interpreter/functions#table', () => { const fn = functionWrapper(createTableVisFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index 2e446ba4e4fcf..28990f28caf31 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -19,10 +19,10 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TableVisConfig } from './types'; -export type Input = KibanaDatatable; +export type Input = Datatable; interface Arguments { visConfig: string | null; @@ -44,7 +44,7 @@ export type TableExpressionFunctionDefinition = ExpressionFunctionDefinition< export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ name: 'kibana_table', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeTable.function.help', { defaultMessage: 'Table visualization', }), diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap index debc7ab27c632..17a91a4d43cc7 100644 --- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap +++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap @@ -17,7 +17,7 @@ Object { "col-0-1": 0, }, ], - "type": "kibana_datatable", + "type": "datatable", }, "visParams": Object { "maxFontSize": 72, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts index eb16b0855a138..e481c311d5453 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts @@ -24,7 +24,7 @@ import { functionWrapper } from '../../expressions/common/expression_functions/s describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(createTagCloudFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index 42e126908c00f..ff59572e0817d 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TagCloudVisParams } from './types'; const name = 'tagcloud'; @@ -31,13 +31,13 @@ interface Arguments extends TagCloudVisParams { export interface TagCloudVisRenderValue { visType: typeof name; - visData: KibanaDatatable; + visData: Datatable; visParams: Arguments; } export type TagcloudExpressionFunctionDefinition = ExpressionFunctionDefinition< typeof name, - KibanaDatatable, + Datatable, Arguments, Render >; @@ -45,7 +45,7 @@ export type TagcloudExpressionFunctionDefinition = ExpressionFunctionDefinition< export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({ name, type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeTagCloud.function.help', { defaultMessage: 'Tagcloud visualization', }), diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx index b433ed9cbed21..21194189745aa 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx @@ -39,7 +39,7 @@ export const getTagCloudVisRenderer: ( }); render( - + + ({ describe('interpreter/functions#pie', () => { const fn = functionWrapper(createPieVisFn()); const context = { - type: 'kibana_datatable', + type: 'datatable', rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; diff --git a/src/plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts index 52da0f7ac14ec..bee200cbe30ee 100644 --- a/src/plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; @@ -34,13 +34,13 @@ interface RenderValue { export const createPieVisFn = (): ExpressionFunctionDefinition< 'kibana_pie', - KibanaDatatable, + Datatable, Arguments, Render > => ({ name: 'kibana_pie', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.pie.help', { defaultMessage: 'Pie visualization', }), diff --git a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index a4243c6d25c41..557f9930f55b1 100644 --- a/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; @@ -36,13 +36,13 @@ interface RenderValue { export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition< 'vislib', - KibanaDatatable, + Datatable, Arguments, Render > => ({ name: 'vislib', type: 'render', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], help: i18n.translate('visTypeVislib.functions.vislib.help', { defaultMessage: 'Vislib visualization', }), diff --git a/src/plugins/visualizations/public/components/visualization_container.tsx b/src/plugins/visualizations/public/components/visualization_container.tsx index 007a9e6e9dde4..5695a84269bd4 100644 --- a/src/plugins/visualizations/public/components/visualization_container.tsx +++ b/src/plugins/visualizations/public/components/visualization_container.tsx @@ -21,16 +21,19 @@ import React, { ReactNode, Suspense } from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; import { VisualizationNoResults } from './visualization_noresults'; +import { IInterpreterRenderHandlers } from '../../../expressions/common'; interface VisualizationContainerProps { className?: string; children: ReactNode; + handlers: IInterpreterRenderHandlers; showNoResult?: boolean; } export const VisualizationContainer = ({ className, children, + handlers, showNoResult = false, }: VisualizationContainerProps) => { const classes = classNames('visualization', className); @@ -44,7 +47,7 @@ export const VisualizationContainer = ({ return (
- {showNoResult ? : children} + {showNoResult ? handlers.done()} /> : children}
); diff --git a/src/plugins/visualizations/public/expression_functions/range.ts b/src/plugins/visualizations/public/expression_functions/range.ts index 42eb6aa781970..409199deb8181 100644 --- a/src/plugins/visualizations/public/expression_functions/range.ts +++ b/src/plugins/visualizations/public/expression_functions/range.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaDatatable, Range } from '../../../expressions/public'; +import { ExpressionFunctionDefinition, Datatable, Range } from '../../../expressions/public'; interface Arguments { from: number; @@ -27,7 +27,7 @@ interface Arguments { export const range = (): ExpressionFunctionDefinition< 'range', - KibanaDatatable | null, + Datatable | null, Arguments, Range > => ({ diff --git a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts b/src/plugins/visualizations/public/expression_functions/vis_dimension.ts index 286804d2fa76a..a78634b7eef14 100644 --- a/src/plugins/visualizations/public/expression_functions/vis_dimension.ts +++ b/src/plugins/visualizations/public/expression_functions/vis_dimension.ts @@ -21,8 +21,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, ExpressionValueBoxed, - KibanaDatatable, - KibanaDatatableColumn, + Datatable, + DatatableColumn, } from '../../../expressions/public'; interface Arguments { @@ -34,7 +34,7 @@ interface Arguments { type ExpressionValueVisDimension = ExpressionValueBoxed< 'vis_dimension', { - accessor: number | KibanaDatatableColumn; + accessor: number | DatatableColumn; format: { id?: string; params: unknown; @@ -44,7 +44,7 @@ type ExpressionValueVisDimension = ExpressionValueBoxed< export const visDimension = (): ExpressionFunctionDefinition< 'visdimension', - KibanaDatatable, + Datatable, Arguments, ExpressionValueVisDimension > => ({ @@ -53,7 +53,7 @@ export const visDimension = (): ExpressionFunctionDefinition< defaultMessage: 'Generates visConfig dimension object', }), type: 'vis_dimension', - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], args: { accessor: { types: ['string', 'number'], diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 56c055b8b1cff85d7dd891750efdcf6aa198e937..b828012f39307b5a991228ee76ae94bb695a25eb 100644 GIT binary patch literal 22893 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 E0F#*TO#lD@ literal 16994 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 diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index 753ab2c2c6e9492950600b52b61a8241c79ddc53..4f728f51117483490b1890ac756afd4c6c3197bb 100644 GIT binary patch literal 22943 zcmeFZWmuK#w=axcga}9}C@G+Hr$Hzw(k(R+kd#hEX^<`{=>}zrG$`GOlyrADlRV?` zzt?%+YwvybIp^E?u)Wq=7i&7_J)h^k#~8mD{x%i}1?bGn6}6SZb(eqK_0E&Mg1D=F;vtb-K}yS$=I}f-Y}uP`%kq`&cwwN>TF4 zUD4I3r_7~e*1k^(H);DHdj@OV6)hc$dL>QH(SeGUYHa#`dV07o>=xu+y;l9Lldn8+ zZ_BuOzxq1YqR6KOX+`?m8Tjf8Ojr8ku(G&Ho-iOaXH zE{BGeHY_qh_W4V!GiOfDU%WUzHz)P*t;qMo)!LEN^ijHvMWxU%g{QKzcNzF%a#X@f zXG-eNjpAJ&U-M2(WI1tj+s6>yy~B#dA}6<*jjTsQ6B_Ig@!w~s}4_=W! z#>Ob!-9?UE{I3&|JWZ1%x_o(EVl(60H_qhFM$3ckg1UOG$-y0R&1$#zu{;)f=n)A^ z%j=Kcy*q#LfPjSL>4u)zl`9Og+Tqd2+M+jZ6odv_!cXUycSU6rF%iFh-Pk`ooLYmc zvcUH^doD{c>nz;R^rdD7hB)D4rfTtcYFZ3gf4u^U25$N~ z$@Pa04#roBnLj}8PHQQPiVn9Gc2>RIOGs?Wy+nyU7yt9P**uE5e(Shn< zHfowwUY4r$`+?-bV$GpRgo<{~XGKoFCB~QUu6K`~i|);wP!LqAW-g*AkPV>{p8men zZ}H{$P%Brf+E*b%cFR7wLwrl6ruBn-vALOvVu5%&SuZR0`{D5L@U~wQ`fU0SPe%oh zEcu3iQo0Y%G$!tTJ&A2!9z_$QTcZ3wtD)059m*)+2By>h&lawj)ElPdHXCh8Zy&=a zCJs_-*Kzqs7fAj*Pn}3mP%zltZf{jfJTRuIH|H5|jz!|CV*d8!0kc0o8=93)5p7N< zrF5L`8+`1h4Q+f!o2{gqg}Vinl9lv{4@GIJ3#q6kQQQ^-l05^j*qP{N&#qyw%Gs4T zZrQ_&Ewc}Xcu5_v8r9d=bL@qAwjCSXC1MQE7qxg}(GgQG(%ac;u9RX#W^eH4XhDNrr6p~dw+ zXB00?P`|(-FCBY=AJm^K8>^!6yjb?Z|HU_KZe?{N_i~7LtA=Q)&q8nHKxY3Cv-ucp zwD2bhM_72YX-wm*HP_JhwHl)Sg0Va0X{nm}L%Z?O_+&R8ZxJU_qdVdc15+AhL!&&y z?>+ke?WK5fatFRG_os@NV<%4nNCkXTrwI#xXDMXx*)DQk6V_^p?t0>wY$vms<9dj< zak8ye*Pu<4b6e0R{JrcG@q868VIeJvT zz1Za`jr%1Z^`96}OC1g4fWJ}j@-t|pRj zp>E)Mzh5RKcMj2|Zm`;a`sO0;{QF)LT6ze9&^7y`w?~cfA}22w4>{C|-3SJZyJz#3 zhwOv#YcI*KHPbiYiY%Y_%InijSn@W>e*H>J%B^?HJBZV0{H?HRo^s)kZeSwn z_m)8v9K}Ukism<++^F%lRr~u6)TbdKAwR>jIGXYD$4Y)SzPSm^5R*uRkI(bfZbTfH zf?v}7U}n3Zg6rkIXUz=L;k~APXy;CC;W%MmAD>IjOHc{Ai=0Jn;a=i1-ubk1OVH+n z+-%#Od#rDBLq9pi#-L{lP1>ETe`ohD@wy9p?a>AP>@U7NumH78I(l1plM+_Nb~NH_ zloJglfbA#Yb)bpo|DGndW^NGC(=*f-#}%by+7}k4oTuH0*~z~h~>609IJZoQZ}yt_=)75FEc}C%Mb2Yn=!C*WqXAu8fx=q;hV0xg~3WnIFI4$a2^Sjmx7^e=LRPvbgYL= zsTHAwN+;Gs^eg(l3J^~*hiI9PRkeTqN$ne0>GM#sg{y35z7dXj`dht}$~wlcpy}$u z!U8rWUjlLm|LT7?{XWG9}$xRkT)z@N+%Hp<`emwT}vqt8rE^3Z-m| zDSz9p@g2Cxh4=4#o}MYzpEJLRP4>v#n)gwjK*C8Yw&+R{agP2(E^vn~? zvHhjh*R#2$yti;S3jqv;V~T@mTa{s}FL)!D907;Fm+saSoTT3*V{B|3a7!p86Wula zz1t|6WN+0uHNz-43eb$rVg3~akK?1)%VMMP`@;Euc4~v#biRD3xU=}0vs-xIC)Z|r z-b%glfZy3|*!!Wl_(lB&nc02cr`AK6Y+<|(>z6wQ|HDVD7kfxA;@y@p7jUKXv-b+5 zeeeLs{+q?=T>ueD?VIkHt3pmVdk?VbPe1N0u_VtsBy1fC!@$~lseV(pn5K;_eoYHi$gx?@8P5`SFV5 zO9yG6dnzgW=tO@j(#)lL>e8m4@0!fq>PUIC?SJ^#NR?}>ygq%X3KF<$-`#@BOKq7V z?CGoIv~4G9#Do)s61i8(ERRHKsHk47tK#D~HzJ@@9Vc8H!DPp*^Zm`PCMV)QpWqG3}mr z6QFGMZAwG}8I9_U>GW`KugP7YY@=feXCZzBbmQd2O|y)!vooV(+>+O_@6X1X{7#o- z_&xc!HlLvCHq4k_sdc^V(G1h_V39YVTGMGHkST_>P8t?psDe2Mjzn zXQ(b;y;?Um7937iOJ3#KlG9}=))dW!Gbt3T@u6@o%m z#EHzBULy$)wJqVSI*y!fLVUKWEZ-7x@_?F;oMbE!eu&Gm--v9KxkaeT3J{KRp)*E* zUUNIytx5N0M=&Tc5)QfUZCF5pUn8FSQ@(cxJ;(fm6sjhl!Zj84%Cz-FY6LSpWC4GTY8gz8nm7s^Ow^fcjJ`Gm2nQX zS0x={*d3hIF=)uk*CXHG*GokakUQfW<*Jr`dS|gm%Q4HM+QLI#d}nX>XTShtk7gI3 z&|}GP;F&?>JQg?LuDgxgkU;DBBxEGq$!Y#@qQL-h8F^!jwy z-UT+vI|Uqw!(lwu!O?NU9=mqDo;YD9>%f+j-{t-#yxaA;2w=l58}#*?@{H%go^MF{ z&ysj~R=+5*ysQ1HFFaj_jLo&7F;^Q+fmU`a^2TJuR15C8a*#(DHjW_2$#8JCNnotegqEWHaSl6sR$ zk*+5fT2*puNGaPBI=4ExCaMmvDg>uNIn$lwoE{x4HkCQF%WJ-J#aG2KUyZ3PTfK-K za5V-%$iN@U;4NWmY;>8eYkV_-1OXw*50NWZv0t{$+s_|}Uuqi!zJs94P;%#~mm0od zEvewTOhrXS6E-EKMML0ipcb{cTXXtNxf;bUFis=lZG{7`T}rL-^VF+8=&J71LtJ>_ zI{Lf0I*$kR>bSo*2&9!#84qBLU@BZ}r3Hd=p`wyhxrk0qaR)l6#j9WU z<(+f_?mXb96T%uYXRj%n0K#Vk#HG*qM~=4Gyj+*Vc_(OerN@(@0{;-K~j z8iIc0au_8y8dW17)s!gn z>V^j>LBTylL1Yz=Ey}#lSkmnSRtlJTgwXZ>awSqojvu6GVV9t&Pa<-t2nK=%366~M z00Md{h9+h$zq$8#M%0epBMCn?iE>zTRDwF720)qa1apl>L$k`|0=nTNgeIkM`x_sKWaI%wxZB%RyD8SbfPCFCVrN%)j}#n?`(BuVCcPZDeH3w zi%fofdqy3{^?Du)Ac3uR!t#s$9G*d6yny|mZ;nhT+oc2kBGcZw+?EImU65_jw)5+3 zR|}zV*WHh8_cBN{OmuyxcK2?*@n9-G99jI|5H6A0h?Y-!>`38Oa;t+;`F`|Oq89yz z`Vy(>bklx|m&$-ICCQRA60A#uMN0_U5yX@3ka z=3Lu7CC2cNw+DlXcUHOsJzg#t>AX~a`qb)^U^fd&~{ep?Ba^XDGfv9dH;d5ct*4Cb;<&F)!G9|GNnBXGlER0bD zR^fu_?T_xQG^Ni~1~=%!J2=O)s{kZvOXRojX;+Z&W!bjQ9+BC8Y|94!BwrG;pA?oE zHXykO1=N?HTm}W2*^|kQ&-EflON6se+f!-jy4>|kQejGxk=V2cF4(9zUJM7Kz}3O?TVbc^DxRRFh#CVM8$P;*^1h5RgBp7{FrH|u z$>~nc*UuB3OKlhufQ3hIw6wD5kzCB*+}y5f?Iy6dgVgHF8~Z@YQr#1P1Yg1W-c}ma zUSmL}*WBbllc(*aqVj*_1_w{Vx1WJK^+YI5o86e}Q4A&wycTd-Se{gggJg$?rvCb%VLrH~d?c0lej&Y&1Ppl*s7D$&zj|oscc?#F$ z-~sVQIood>{nN@9FKJ< z*lq48^uRqO_7D7Wg0k`aMGmQt9kFXae=dQY0&S$P9AqB9zP|oYH1+crF#CcqhlgJs zYgGWD{0KLGniNfTfK^7saddp#Y$n&|>LC+b9I6H+PL0yFM&QY4O#l$IhQLG*HCv!$ z91tTYt*`XKN=Vc%{n>cBnkKQ=@y(%>R9IMe*u5GD>d?cU0W=M5#!K-L{-8H3uoRdN zHz?g>TBBC#bgF97q&9|2-*^KTRY(@K@*`%F{HiC$pz$3$tyVHb=|6nI321^neQ_}U z3|l@}vB=BMA&e@DWvJApz5~|M{7V14uF)Hg^+WhQ*{_pg4DuBu5ll*{mJ^RlQfPrA zf~!TEB97hAI20SV4UwO#vBkGH<}NZiiuaP_;)Q|9G$vLiuq~RJ%6Rm>6;|&iR5%=^%vc#VxV3L z)db*Io$jg@NjMhmv9G9@$X@IgfcExLtiN$2h&xug+&h0R^Mk}3h(KOT&~nJrVgj(eDbu7(LX24qlgO z67Qf}wU7{qRapN0!N&Uf57HC2s%RqH-CJrCM;H=d#%xgK#{o|% zSLv@|USpzo5vX-oxO$UC>t~p)X?KnhF;$Bfki?7(4ooE;t7+3`x2L=66$X5$2u32w zDRDV!su#aN+l9ESWmH~5E&v6N+>6zu!s!bUjLPFN4LJMeOMB65b7697d+l^iJM)hR z3JovgFg!|n>IU*jIYf}fzJ?Qdmv2+^#AfT zCSRt${4pQ&>S(2ZuZb8t3m?M;4)EN5u--q?0GpDjO10jbV=j~kN_+IqhT!h1z>m}` zDStDbf`S6BD>OXrF;Y^+q}wjk)JRVH6)o{h=9b&9=@qGCn82-W%mq^uLkUEd?e#vd zY@#m}6JkY-IPbFS9z6vVd+TA+-alpuSB$DSR;KhLW&ySVs!SD3E);^R|2lFq#QiS- z!^Q~g-00qt4a7^`1W~^|x7bSqo{ASKFhsBy-sSdud-g=zw4V%(RCETu3moYcr+3jJ zpSyHhx*JnEqFlJ{|~HtpD{#o5!9#EA>)y zJE^+G=M;DMNh7HJCT5U2$)|615v;!&x|iYpL5`kIX4~RGB?m-? zj}M_Jw4y^Kj zQBijN`T=2{kW+T1P)=RYcC-cdul$)9g*RD&I_O2#PJDK4q(FRqFvd=vSxX`gAEH1l zd)s5Hsbiu-LLNanqT+`HufSX`)w8sd+E$z$CTYRL4b`1CqvWQUw5ntkU<+httbWROx% zcll2>u)!Lt&jrI9kOaak0tquFQc_as#0@rNL5_p%MM>~@9JN$cpOx7!wYA}8tV5k| z#XD_d8Y>U8BNA*aI(exMd;D@Z(e(#?RLh<6*%Aa;ubg5*eOV!qhi3_ zHq5kwwn&SCo}xS@R^hpHQnx6s9gqJiBxc+)8T}LPk?s&5ivod(*S5vU=9QUWoJU?y!?}< zppO&+&5K-1Y$?WJUV`%FbfJ}q2H%Rz1755_k6jDo=?)q!W6WbH;2a{KT0x3hdcoCA zHR9H?VsBl|Y1|{@LTd)lb86rAIiN$}heljgU~o+V;0YbfkK$(9L?T<2IeN6D0>Q1&f^}PlIYXVplH!*&2s~EaaT$5oO&e1$Jjm-QM5A?qu;> z6$oNej`?uOv+Cke-Q76MbR_U_Y!Xfn#E1kB*r>>~+YdBxa5tHt79JYN;%>cSjYeU%_K(&&kt9wPzbDWk!?>j(tsEyz)ILjErv<{ht z1Q~U(vj~k<+`q?!VD7n#xJHc~LcB|PCzY>uT@*I6mw3#7Wl2#2L`*Tdi|8Qq1BIT* zzAdoF3O$M6-Q8B=(9iEq51^+P4adn)>Ey8-F7qS>`^blH6i@IiVr{aadY&WAZ&Ji2 z^Z1& zR6M`!r}_D@COoM6(Y%iQC>%=e>$P9*3@|Y+-hvWV_pkDK6WZ&bJ@K2vkb2yIdK-96 zjas}wWrL}iMo3hph}lhOrFSfj?$|;~$-sV5IE=fUNI-9hSu;R?Dt%405+&4uU{NB= zsUPl&PCY4?iQ*JGZ3{5}DUxzeB3g)IICTj@3DeN0y+jgNuLC(prqNi=ND#~u0U3+^ zGYc7uh5B<8ZTmCZq;1*qDP)Nc;v0wFiv!9B-_BOdZvwiuzPU+-5IZn_N{KBfE#h{d zFY5T+SU(Oii`I3|E&R!%N5u;f%YZQ4EZZ4-JqgZs+dM!y$egsMZWeR_tc4Rq^zo=c7}5D z-KEvJP8V1I=irp zo9{`X>)^u&v;r*rxQ*A{4dDy~UQ8G9mc7-Gx@lnWQW-d!YSn9DqzkUR1fA=|Hq6`c z@jE9gM;2_+6KiXYm4q7`4QduPCP7RT%%|5^2EW(ccYaLesP%YK3Dp+oPl~Ahq$lJw zhw&ILKcwBNCRAqMzw`hHjvHTXKBZe- zZi3J#C>}Uy0z_ocgDW3^>;aKbxqW&o`RyjWzWgB{#IFvx#YO_A0moG$#=7ukedDfd zYLqKl_nO7-!p9tNUlGv?BMAgB_Mqb~B;|5s$gEY?mbTxjSwiBSkrDAL_&WG(=?)uu zge=P8>Om@v(Cw)vfWZ=Uwo;x)^c+%*p;$p&8F*fzydS9M>+1_euaOA6e5l`YDPB8Z zVzn1kC(sI?utza#&Ax!|&U?v{M@^fHh$g$f5Y#HYw>%z**k5A!*fhVQ=V2~P|TwSH${!8OHNNTyio*PgtcBY-a2b=6bHPE00 z#bx>2s6wk1_;YEHE6;Qjw*3TpQx9>MzNjMSOpYY95;>1+YY?&Ds=}HA4y+Ha{?804wj6Me7kjcieS8F92*y)WhSJ8hpw zz8SINtU9QPT6C2)hiMBX!ESA1M9@7nGSZ6_%4Nyod>6r=l`GNmFcrj}BEFN2s3}sF ze41b)pp?iO(7C;Zj|MYFuQTY7H3$4LNOwTD5Cq+Wo!=GHs&1aywa22x$bP zkcS%d2WLD{GFD39GPE&j=iZ}2wd;I2k7iW7Cl^moAt73)MGAcXqokai5opHyswLPe z(2epkishygDOA1BOAYX?HZffKs_=bO6!G7j&G$LN5QCxNgD?pw6QXe{$6qDbOl!R? zta^8rc))q#MSb`{*vN{#THo4w4P7iI9xm>0PwdthPF* zf{Y%)G#JO@Cy)2(BbY=y)N=ZB=Vsf-tQyv;1xWau&Z4*+7N4=Vf~g3Z6uq-Eh*LrZ zJ+Z_iQ+)8e$O3#wPd`=vYdaC^7T?MU5%1Js92@=$4tb5S6UdKdbG;$gTOj48oNaKg zL$%zEIxDN#O6>7tsv!g`!DRn|<-R(TbRLl8V;>1fVCq=RF!ZX;C3RbFE1_S}LitzF6QNW-FR$;)X zV!-{f$coTgCgD$f5oNX}A!9@glR$E7b3qkUh`8SDD0ALpt`!exoUnX}AR?&Ky~&w{ zXMu}KY^IPG(Sn8-;a&_H0?B!xU5)*h2tph6MUjbN9T)U304eKpfiwJo#-^_tbbDi1 z7hK~uV2Tbyubq(4lc`OFMmo{W3MiN%dW0G-u?mCPn=fF9wq?I4rBcdKZU}((wjO$b z)$~u(1`UmNeeMiX)g*}M=w1L)kUvy0Tx&Z1tT`|0ulD3C5x@;15@CQ5EZdQ?y(wU9 znr!yFmwKEk_%Dd1`4L$*99VFpVIrlirK#y<2YsS7%#C#TK;cDkn(PyHApY8lfSo?v zUsF@lcmd*bL!IkNQoyrY9)8bJX1CyD5Qyp&`MXnUl%4u5J$RU@pmKwTJps4RhD-f0 z(9xI+DjAvbYD2~W;=aPy4cad^fSN|6w@&GsIb7JR@a-W*|eQQ*~dI3fXnSqGw z-HezuP)4IDGW-W%eX{2zHdf>t!%8-mJ8~;TzFRdhDBQOsTpGfl?hI7BWSNlU# zX3~#X#~~6yZ7awZ6(dTOkg%3{Y4XS4MhtPJJ0}>1ur%9sbGL~wla&uF;|`F z@slSotWC#3^Q7aImECc13_)jKTDM>$PXapAvF?947v-;shQQSzvFFfY>b5z zG;A&;;+<+OsG5a;(*bclMB&!zeGQIG5QqK+4D#Wt6q#e#F+UL>g#kSJB-zUJXg2)= zn#H~>u}AjMmP34GH6dpkaZ%BeS|sB9VKO-wJnl#6xkgt}=vgE>96D^N4=E2zow^A* zrkLtzZVYU)AWr`g9yH2pC+;vIrZorRY|9ZpphhOe#0nBD)?Yf1(wG9#T^~(l!@ezy zYs3c~{IvloWpd%ycTil==1XK&7l-k;O+Rg+vGJel>?O$>{T~sh6K3Tskr@SKa#k%1 znj<|qd*`WQcA6_Zqm|%v5R}Eo$KU~brJ`FNu@7dcKKRu3sz03;SbDVrXfALhnj$TV z!;~PD)v~}4Jb&rZUo!E;8~d~U*t_Wq$TV}Usaf!H7nq-ay#b<}CMGXj(uW{K*$_^j z^yK2N^0kMX9?<0Po+#oj$$p;n80IkVukhPIlOvcIz|!dG&jk1vPf@o79X?FaQ?cbp z6wCVfG@a^0t&G=WLCaZNebkttuvIMsknUF-=9 zHEchF*@=+l?qYj;My1?`+Uqa2<{Z=Nukq$Uqz9051PtGV(Jaz#`iTYLBFJ~w?Bh=p zaiq)D3>}^kWjBQp;eYK;Po|fT&yfi7XcXP>(J@_&e1)n!dkXa2q+<`5skDg_I+erZCe*JY-EtMK^&0h6?r{&9omU$%eCn>NIXpDdzxBae9 zM7rYBUSO`ULii#>L04}Jd`r?Aik$ui{T8*dSIo3eo;2ZtA>9f>r5OkC8D?&i+$#Bf z43uGb67Ze`jbs({kzrp>W5EpexngA%)fC`O;dGq?#EdkFNGP27OY}e!4p1ay;ubQw z9ui9F;`Um5oxKFWRnrwX!oq{DoevR4PBv=+ zaD1n^aDfKDN6L|zg}WRrk*?Q~85#$O2&6?WC8E)sT%Q<507wb4Z5HN7#%E^4>>Sdq z&Si@zN9<*EBDKo^+_8pO*YYjvtvTrAJ+$@)LJjJzv_5{9LkLsxGQ-hcbn$HU{>ug7aoIS3bw z!`?Eax+bhS&JGM~_KaVgadKN5qGFJ@sNzzK@%}jf!q`r%0-!ORjQjOxshLmOBeu4& z%1%2w&kj%vl3G_!PEN>dPcM(y=u-}?*d5=ToYLEwxU^=n=}h^mZ`rXdoTB8^eB>bG zCp-3&AN85Jxjof%a^3YN-DPQm-^>}Xew@zIVquxwqo-Hl>P>w{eXI_xpECPtt+Fh4 zoiv%l=Q=Ff=`}Tvd%u5q?rlEr9B*E|g1T_&(!z^t*RJ(Su?XgA3w3i%)v67ang*G) z2LI$)D6k)4_NEps5wuwteiYB2{tCp(L4Ub%i=d5Y5iMp>d2uMC*l9;PqPRGUx#-K6 z=at(v-HW{(S{%Z0Y48|H=67sTm#`D3{uOK9$J-ij;6F`IQnu%Y%=@-UId+;K@(8 zXgM#URA8Q(;IRBi=%DEp40)<7v2&XY6h^DE#vSC@%B>`? zW@*_h?_Y66+oaue`TA5Mhype+A^Nl1alS(5>IfkT$sJ8M?AqshJ(yECUXXrrYv8)tXN5$pKlgRYlIpCrMk5z(z%spFsT^yU#~ePD`R@B?dXZLOeH%h9oP*QH$U5*~_?hexi|(H!|S z3yU21qJ9BB&ka(gWTO=DnKQu4H2HN$9j^E+7FHAs%NeZaPt?=~vVBo1bxSX@HFzG| zE{V?vrCLpSnGBaCy~L3 z|1#EFvL->Br(9h^Md*LMW`Ml*r+DqNNwR(;;Td@G$4t|8EG(`sb3?8Nv}WqXikY={ zk@uF|B0Y=s`J&zOpERA{XbF{ny|)G4TRpx!@P^-#RiQfdO~x)6WK&-@YR zZK=VcO7)Ns{C|5r3JYuh!ivJ6iGBn7-z82BA)!>2AN^FTJTAx@{9*;q@Capuha2Ej zFWGTrQs!mvu84fzOPB3Gz|kPQt)v1*Xm7mzFk>yNVsF*)rR~w41@&rO|Kya#Y>B=6 zvaOFAh9q7l3Sweu%DI|pwxr|{3030n9=$W^5|n>`X|F$`{nOPi9xW}BfzIX?xNcEd zmrm^x)r%xwC>`!n{&H5xFg^Bq;P){*hhn6Bu|vQ-LYJg0_&<5O8MM`le|Ptr^ez@z zmv8kJ8iqpcQR<}7x?t*bQs4j0V(g3(k4=*dQk4Y%i<>BR-dlRVKxx_h_Ggt&>Dfj>`A~zZmOGE0VqvvnCcGcxP_jvy z_T{U1@2)^n@IFv0dvT4JS-CS|gpiQrUN>wLr`|Nl%pCEnUq8rUdr%z-83gpQj71to zj>|*t52?jJ%DcI}yNFXE@1^@{cG;2NYBKdjxvjF9)6xoqlUWTp{~J0fn30O=Ce4Q_ z15-c%ljkCm8&)#g&i9y@6v}ahW|o#xA|HIZRuT>)-ud>UG75IG`7k}0DUPE*{ng_1 zOdtuLI0kcpjB{O_)GH|lJ-1Nj8z#BZr$&reE|pv6p96Ov*Q{b9yqC+Eo6tOGW;u5rE^4+PlaFzW2~C7_QO%tH%8 zZB3t`7SEbKa@<+#mpD1G-Ntx`siWzh@N8GgLVjmI$mutjA3db(gF@3&X!wSfhO-+2 zoYQ{vENpD1tE9%jYwlwT@zKN2umR~yPJ1`7#@)AWVEqV%#k1R4GCL1{1F9g!uDh<0pV>exyGPGMce=40+b>Wbv_g8Bz&Hq zcj9+DN;Me*GQ>HXL{CUboYQ>zDfKOudyJ%1E2q`(p-)`S2_V%!e{VKZ)PfAeu1z0& zE-%ke`{hcMcVQuWJYQH^?1_`N*O+a&TT9P3JVVLYO)n}cO?EM{-rjF}BEP?Acryw! zE2pcW=NRwzBn6?QL)_!}*};!VCDwM)0@bG|S)QXr4qn-3Hawvu*ddvdej4ySTNTyS z(|yB6`*H`idWxhj70H;VEHW}O8j^7LdinWDl6uKtz4h03pOt`m)3G;3sq*sWXD{qk z(8(+@fDp57F$LhGAvo=5UfdugqFp>J_?NQG+e}fkv3lZIf!QUL zIVvdTw0tNM;}zuakX|m~_wGtL=PG976YGY|%3zU%ukV$sS8)3VZOI|aUl9=zo5G_( zpE!)F$hiFEYojloH5oD`IMIlS@rGWT+^dOX+Ol*Nd&n|sh5!0qdjIo}r3{Ado=EW9 z>}UQ?ueb}0jKZ!wi91ijWH#54@!q}X1qoT~L_|bOBhAK6t~KNcryQ(4{S19@A_4^+ zidqT5&uwkBu$7UG@B|*`@RdB%IZIc~=m^0P^`{C3M9kn{3D8})LR7FjOD3x}lziNvH-x#r7vr(2Z;3l?ik zB~TVg0Q^Ch)c0|DPWbRsTz8N`Jphfav z@c&!)hJ|&Cx{USm89;fzzWZsci>vF5)q1(~%xozU<(O!?0=-;oPr9v?(v0PYdv}v! zKDxO}|EDh&C5SGN0jw774KTq#5-8`}=o;tBrZa8~8|5HA2ot# z;s$-AF|vAt1R97bbd(fMF1e|xqJb1#FVqJ3$hLkFAT&sDbJ!dKmh8~bEnv_& z70Hi5FHxC|7Rty4Iji}qXl3cTlO-!;pl!x7CBh@(sjKrWJ1tc8W?zX#|(Bq*g1wR9nBF8aKgQ%aTENui{DIY zPmaf$ZR&}vvVFf9Q!ci}+^;;IJwIaKX*qJ-FR^XpkVMWC|roR^y4Vdb-(=T~p(bhU?*WWb1;WjiL}d zyP)x`z2Jr$d4_C!h;heor;@5_=JN8bMp8e>jJMQl8F)b}E28BZ_*&XJ^IY3zc{r?|_A5!j@BciQ0x_TrBBt-V=5oU`W zv%jB7_uXtP3o7E@D8*TnSn?9CECT0WD|b3aqj?59N+RPU1CoD+bJHy|)>lhA<>hRS_MaVZ6%iv?+oQ2oks%ZNJv)1; zpj!MrR1#Zw28|uLNjkf=Q6{*7os~Cvf-WvD+Zh@c7Y61Um_UGm0%8_##AVog3b?=^ zAfU4Eia;@CdvtQs=Rfs?oQ9kUOZST==yx-SW(N^I7?c1bTr#{qW7{D%Fp|Z@OS2Iub<+{P9Wgld_MFm*!7%atST7 z3{ez)(+q2-b?YKIh)QIbn9yaPnVHEdi;L3~ljsd&;Mn>?C+Da`?RBaK{h^+EA4@(W zV&a_a$G7j61;cYMP#pR}aAeWp-a3*EB&%;ip|i5*=U>I(Qew2>3`~G^-4zkhaqQX1 zp;j<7Oj|$9R%nz%$Y%DF!|zB<dwNvv(E-N|tMXO5c(pUNfC4&!A z_Qx~aT2DW;W|fqvDiJMS?(&2OLKx}NtExuI<+!ZI$L&pe?um29$|#9RNVMAQH|<&T z*CJj6E)>d^l#e>MV;Gs3E?{Mnf`-1oG6MGw>B_ZOg}?abm@Au+y@s}Xb;KD+K%k(X zpI?z{BQwseUvlXXGSVWBqB0B{HR67`#DjXvNMjlTg5+$ z(Y04S=BmG+0omywO?%8?DV>aq7%@8tk}{G9pXg262eE-V<;dO^QBnegyF}^s0*6d= zY2uOuL;53Ev!RoS?ADf+P=;*nR6#!}Kc$GFm76DP_wFes4%(Vu7@mu0qhN7ST{iQY5oR zR{4o_EVBW~L|Zm>PPv{>MWle|%JKYe+1V8pns~xTsbri!eYJQWUr4_{82oV+ zrs{U@jTom?yATF;_QwwZ!`**A2GgcRze`2RU@>G+tSC*VdZ~!rH4x>(33(G4xzJub z)(wf$Q>Jt+M!iV-H`LmemW~kbq4zMNz?~!=;qxu}WAEIYdC$T1P&R>qmE1CWC`pX1jzuMEGiL z^mwq`zWU=WKKP(20_$9d-I&2LTknd>%J76LWdi!jybm8xWqS$b2Iv6;K&jqkxjECN zzT2C-yDyv$)MetfzXO&oj5@D|BAT_NOhyW+d-NQ0ul80gX6)un4|labt#|T#bE`$+ zoVJ>QHyS35WC&DV)DW7X5M`ke{$bn zs_u@-eO-CfA_B-y!@^Q-PR^}}Xif%ps>dT`cA@YaGw6#fyKap1R2P6Y#IF7IJB{hv zo4w+zSFyuu@sof2z&cZu1U1}t`Ijs-I}{pmS9-oa5#aJQOC5MKYuYyN%xPfh?)bse z(_}YetH^c<*U{OTLC7_aPS!~S;ks)=_RUax|F=u@Mqb28Cb~tK8D!Mag@k*1dv{W; z1Q*C8;e%-fTAdNey?|TH;Ck5DgtLR`+%bG%b7w=|Tst8>U8dhJB4=lwI;9u-%P}1W zh6oCRTZV>h-vQ%nuu8vJQc8-;rU}dQ{3X+sGMtt+!Q3G)oCpauwbI||u9>?G%_gIQ zkJ8VrJiv`WIZ0?>iWmxGI9Hno5L6~A(K=+y~P}E&R5s-#y>xF6@2p%h@ZT< zc>?Zj;p{A9j_oe`L`Jav%Z#Qi2Ufh`8^huBA^n02f`Y8KHbF|3fd`l;K}nj2YX8(? zL&qc)%Wc*Rbsr&oh^Gj+pro#z4V|TXG`y3Kp_42gCk|0}+VH!rg$SHo8k~cHf z8#kN*5bskR_wP^(<#|Z4zV!B?@WH&WhU)EH&~wxHAUb69$c>wMn6~SqbWFQv&A=CX zHl^dFTva8U{Q&S0ARMakIThH9Jo5sgpGFNeI=abm>jOT;TY+knIg_0NwGjjRx z%WX`6mjJ}=EdObQUw(Bj2=!eJhxOv3m;4OYj;0^M#FP6NonclBTbj%`CY}g zM07H_OxdCGGPuw1^}jzy$WvwTN!XDDjfYLxy5O$}7gp(H zN5EY9`NzXFyOmTKjqk=2HTCK-RpJx6Nlu%--o3(Ud4fm~K6_350E6;G>bKcog$zh& z<0Glgmi~STiWP-Wa8#9ZRnrP?vh;cdYW$BqDQSB>b7TLivzCb%yNlPvaUVc6U zILNRSICTSTrj(vO+x+ETP8EFm`gLM|fB%QuS~^-mu9dC80~)>* zBaE>>{`TDT{`LKTUSGMm6u2yB5wOl}ZDD!+e2%_u=sxRsU@zK!DKJR`GX-$x>@WYh zw$*;XTmm#+km137VEf#Gx7A8!RcAoE^Zuz5CL~;S2?$tF=b4bukkx%yQ0YOD0C2jl z=<)OC+Q4S+U(iN2;LyTfnfUnltJ8oDfPz=r7g^WpoC z07D97_i+|mqU)8oYf&@HW L{an^LB{Ts5*pn>~ literal 17033 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 diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 44226877bdc5afdb3f68b2d29cb03bb4b8a24166..0a9475fc710d177f0537959d4ee0c5748692dcd1 100644 GIT binary patch literal 33019 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 (uVb-dudNnQ8 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 literal 24240 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 Om~LLP0HLmWB>fNO{2NjL diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index e0cffd065fc4ad31a2a7f09d7f58de66722a83c2..aab2905cee19bc7d02eabed5e1886fc3663f68e0 100644 GIT binary patch literal 4518 zcmeHLSy)ro7Cw|JwOmUrSE(4FR106VNQEkd08z1x6eYD35D~CNNLwY502zQJPE-_1 z&%+!60qLg|22Efn$Xkd3-`TefBC{&d+&d93cQ%%GQl?f$1!0B#C{Ep> z!e#D#OsQl_psAqxem0M)`k8_1v`@07BN zy(HphOSWpPh&-M(&i*aJLv`#XiY^8Z&=qaMcB(n-_dvm)!6J7-di7&e(){z@jj52k zyCM4=EbUo!%hjtuLwfFjE_Cn|SAH$^zw5^*b>@JJ2~n$n(;>SyZ%gis=tQ3R2yGhG1bX=FeS#x zSfW_ASj`c4edJVL(R&l6I5teNbeAG9gs5uMo`Rs{AIHp9OvQD#petEXQf9NoQub&$ zgWL+w>l+*_azU)%^>w5AfntvD31zRcYR_iTFgVKU8VM^GHZ z1^R7{LN1@uHm+oi+|MF%m#b!U2>!xYzH89d1ipqvJ}ydxFP7X)d$uYJ|JO@_ayN}A zuZd}*hCB$=A21qM^eqlN&nALKtRYH^cG7FQJ3Y%w?3vpk7U-n~?dVA>D=xocN29JAxLpqe=^dpEifGP2vt^B5PF_KN zV4%*u4g7iHXrzLOT&~q=n)poG(fVDRfBNaCzPl+aX@)#>c0ksWrh6nWff>0}BjzCW zIi@XDZkhfG&CiO3fk{WiM@Y%k<(nw{0>&E9A07}~j}dn;E!q=?MPk$~`6Yhi_R$9^ zk{lY7#|b#b9hfxX*y>I`zIk`RPTnzAY?Gl#w7*xEbj1o=4lX z-Khdt`FN(asXtZ8>(?hBjVg#S2g-fmb}Nc7@6JBGBvg<ikqUe#Pxaq0vMw zZ>A|G(*hrPSnrMTDr0_Li}2N}%GuAJnFel$mzFuA{dd{g+Q#_PumKN{4Vwk7Y3Uo- za6gQ9d`b8ANB%OI4Bk^++V7kC9WFfJ<#O4MrF+$J4Li^&WDlCt^4jL`m( z=reiIv!ctmmW7uOW5XbLR&I5fC99GQ`~>5VmS-%Y;@*g_UfhsyZAYSqO3^qka#BLf zr!8OaPSf3U)D0!@{L{i&MNV}uUwY!DcTt!ZB|Fs5r>DEBBW`0_+YgoorDeulpL^9|U1B z>G9m$snsCE4xtWq<&}PV-7kzvJv2tLz&9R>R3FYBGV!o28)Nw2Mqn*Qjp4D?cp!MO z7x^U^JA!7cd>w+I6{6+m<#Z9vDn|;HUTBpzD_YT2g=Eqx^K>6|?nQ$NMxMI$LOQ~< z)ja8CMqoty(P(r4VRnp>DsN9#i7l`@RaM*UoMEsp1&|qO5=}=gr((nzeC_U81x$R3 z7xqSgk&3%^e`Z(mWAF;A)2%p6^$LbCR=q^k9$U<5YVy%Mmg~RUAY`$!qN=-wQbzmx z`x)Njr^2wNrzs=#7+moDWuzz(Q~+HMS<3uZvC=Eq*C0+euBEF`$asdGYqo!DtHGMT z39$=}tlw$2ZR$%^3|4g4bk7PQjOSp)2L;WP75(1iay%Xi98~8)O2(L5FX-uXu1E^6 z_f66rQfpi)YGp=;k;as)X1u&E{T~jhRpmQW1J#YVj3Mr5Njb&*P+hFB0sLX}Z-P$h z+wF^|R*MH(|D=Y0m1bYuo zOdMO(jxt}$+ZDUP%B6ODQgqQVGwt`5mX>rPQPuWh?2)Iu{by0G#d0R!4Y@tCh+zrxjOmY5_~Gd2Xpivc zB6OcjR$kHmM`~i)FOZk`10_AIid&Vs|44$FI{LcZf}U`%o6rA!$%2jf>sI{ld29dQ c?~FMs@R;Ft*cuu~YQf??lv5P84^L>lmdNFQbpc^O2E3Xg`#Xo48c4WmT?s5Bfc9tTm~ aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b diff --git a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png index 6578f8e30415d5a36ef87dce4592bb771f57f9cf..2ae380df282bae5bbe59bb3c7b682d91b12400c2 100644 GIT binary patch literal 46289 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 diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 14457f0a4d0ab750bd49fb09b1c1e946b3611365..03cc2e4d77d3735b5ab8c2ca233b3b33ad845652 100644 GIT binary patch literal 32672 zcmeFZbySt@w>D}6N+|+LDj+D0bgQINBHbN|boVRWNXepAl#~+bk`@6G=~_q#(kyb( zXFhN2efIbL_TJwaA_X7On=4smPQ>T0_9!lOh~pIRJiRuhgbrYk_qcuKN-S~64Yild zA6~yMp1x?MMY*G2Y%CS8Ml97Brm3kR`Cj9^@=HcF53Wmor_bDc`7(*&W#Yr6+0~PA z$DZv~J>G_ndyGkIo9EPyDe3(NMXCy4$8jQ?L@3$b5FNQOP> zRlDf`lEh9apZYkj?C>)hH*el-l$4gfa-HKG4ijSW4AcHr%0}teJQ&yw0-6# z-QC!nFS2jn(pkQWm(|v0HZn4jHDqC9q37o(O3Xi%-3MzX$?gjr(|h18R*+>(?6v=XtWsI(gn^ zzRe*B3%5>X|LfUO^?iYT3vWMsipOHzD0o8X#l+GtVC-Ak+b>g7OFe!}On0U70r6$b z*#=V@!|g?9R=sjR?*ncw`}^~oRoH3q-nHp)%i-eFFJAwieK`1G{~6n ziI4|Rurf#8Bj?)fxqN}eq50=ug=2a~%P2MLjd_>&pEpS^sV{6Tk3^nM)O!&AX|A8| z*RNj@UiSEVdy2K&-~D&Lzwy>N6Y9OsL&!rY-WC)1H~G14inTP{pnL_wR?u8sg<0@=HoS zyQifk$ZgSMGPt~FYBB%wQl01a&z&)ww-N{C{k){4q^t&*vvT!%ZE!zKU|S?57xruC z-sb0H9=S)7&j#y{lDtIrRMbD{`g63jw>nHXYuFz-Ys};r(7uqN@eFca@_i@tibGFU zRh7Y_8Sas^zlZO#JRZsMu0noh?#&R_&yPNuucWLww>PiXZlZ%#+f!x?-nbUgoI}oU z&ut?c_Ibe|Bs%<8^-|&V{!(F^a)VgeK;gvoar+?&Y+PA>A~6NU*Q8=wUq)J`<9Il= zpO-N9CAzK?YP!A?18_Roj91CXxU6=(>i1l(YiY@>2e!1FyP!W9ZBN+%n2c>s;R2D|U;o7JWH|I<{3Wii;nAd$+tyC0)109Cw1b-X1+a%>rQp zTPim1_IRQa_fXc-Ph8lGA*DSy`gQdM!rw9u4p*F$tjFZ{*GcrLcwhPBl&&ZA+3=_E z+nP)@wYIdieg7!s$lCd~e5A&ec9KmmO1x@w=gm1BoEf=tYwm?+B{%ziO+uQkCJn`! ziRb9r$iPeFB$SjAon+l5tl_0^-oA}3BKPhrmdP-0Zc_j|PO&g4r5lg!q;;h9 zzR$nahjo)!Uc40U_VdkDd_sbC6PT5IAM6-tni*kZ$&_G zU~h?u@S(Dq*)4kMC`RFf+XK9I_rzo4;&SrU3w4_%h{QYEXe;cpQeA4a$~@QrKVJ7p z)!`BnX7P=bn=pwV7Ss4T%&$n`5fZj({MYZ8lOL^EBTx63J8#~duB4@P-Qp4amG7(B zUT!VCF#Lv%i>o(8c7pUTU%RB3`ei=-fX1wE&N`JXoXoeqicRNba1b(yOR052{|HZ+MiaWlurF@WOZ5D53Tn;mbA1KJ<~x+ zf;QV-#rK*T)Umn2%6ip+g@eOlHrxd9O^eRxl=8>gPaX{wrr}+@*p$Hv(U9TGMPhJG z;5BVW$HRW9zO0_9_q{U$D=vLOt=`ik=Kyf&v-q5}TuF@DGB2GX{BlJ`CL3zdGR9d)f7Hklg6f2Z0W{^1E6G(*<{i5^OU>J6eev^?K7C49I1t8 z^r?6GHV%-b22?OQlv|J1M&bk!w5vV7XFcUqos<7BUiqWQyTVD3BicPJ`R;N|_Lnci z;UWDBP=Qzshm!1#AAB}3HKTr$Rm3_$R;S(bfOh{kPZ@o{& zSj!ue92-*-T(jzqnrU~;T^e7#(iY);)>mC$|JxVx_TuFOwf^|?m&iBAH#tYDmq#9!Kkb|6oGJ+IkB^8D9e3_&hCq?CewnfMjvYCE5_GbxLdTckX1!gJF3e07 z_Uj_;AeZs1<+ITvBGJY-62X^as@oq(&f+t(vCK3YHFta!dZ6HWFhPXkKe0NT=kkmTWyHvVrBkN}49=WwG#)RNl>sj+c#P3DtiqTftK>>w5Z>2L&#w^o8I z_tP&H_08SHcI7_4=i^<|wy)?KtpCM3u{E`|UW>`^JJtW(#-|S-=*R7MITbTpRP@GN zm#0ISGTLc}%e&7u2dU^jr>Aoc2amYEwMWkVMZ^x*7c(eK`}pzWkcR!C^bGg;IwB1D z$cSKs@1|%AKSckvqpxm{QlL?CXeMAeeaU`mRpZ0n)m31G+=EO9Qdr!(h7p! zqure*`Z7K`Mn;!24X#HweCh6<+2k<)xu!TfuabzQh5ZB!Hhg?sMC`=)!ho`tRtIc# zO3A@-6S0-#JDUttn0d)NPmE@*WCno>TE^b(Kj48 zbMZaPjjK^K-HjP$7W0o2YyX#@>L4hRUU#sLq~P94dwQ>_8DFoUF+IPKe(IYcA*rWH zNfRQjA6yu?OiSK5S$M=UpEeXG9Z50mQq$2P*!FQvB}idtUJ)dxZN*Cq2E`!GyQ zinE*B)3%sFf*}Km203;*LBY_mAp*I(fF0iSCaV@K2PQJgpw%kEKjt5dRl5%eQ|woY|ZD3m5GdUVprNOX^o4; z&z>wVzDRHb)plMDy@$ItdX}+vg!k{;Eo-vtl&>0&ZjHFHY<|8u_w69es_1&t$fwokQgS&_=sFM1pC9EcM|n;MXNoZ?3XGJx()ML_H!U!K*0F6kxLFiQ z9Vjm3JsOD@+92p#Lo!V{p{7PN2b0DGeNtjQEL zPNO{_INYt5Z_BHxs%}iloV4fwg&22lXLDK_ObTF}Cnh6nJGsrqcI&{B&}ztfcEwff zGiAATsU^l$*L%Q#IHvmMxyv!!h8-21!{y%OXo}+&&SN1r;x}*J9BLo9SXVCpkQ0Es z%~cd(0Z`#)au~3SwfmuS?)>>F4l-8V2!8E~NApz{H1oBa+L(;RflD`eZzc7|o3dA7 z>FQI(!XMIl`Q<_ywLd7}S8nhTF#ZOvH8+59%U4JljPRIxnw*`rZpEhat~b+@JP7T^ zX27xkip!*ZCK4jH^l6`ZwabdUp537v`wZF*$JYfoj99YBd0T?|4F?*JpEkxOCfG9d z`K&(3vg6M4-CkRm>@Jibk!|hnvFZNqnw;))Rtx|38KW%}|I5iajN7%%MYr04ECnm+ zx^WG$|e z{ou0Yv5ZjubhqXELHPB4>+77yDwdYF;rW*7$xRtJTNcv?afpA)D;yaMX&7viLkNSA zBzP8pK9Zk`5GRgN@n^Nw$s>4I!+WhKbZhmZQ8)c@rXsD!TuYorE*?sO_h)#EomC-s z63b8$chA`TX4OiIM?^FeH5POiGcm+vHpbVf@j<$_)Tf#8W2xhM+vn3q*!3g+r?3QD zJUl$3FF)^l-Q5#~r=i_Unrh+`6IIA-#g6q3C;S2@e-^aUxc@PqL8=HHTgz&T$|mj9nWVa8Us@PRzf^I>A&vj6jUnHVpXr@5I- z*AzJdZ6~3jX`_Am{YE;uv{>sca)W?e0RKmi9)Wu|K9Fo_-&yG~?Lm9H&6Jg7{&r~G zQBblhy|7=}>4#@=ucLTk*xOB@c zvu|(Z`gRj{Kya`!>B?By)I^0rGi4GEA*MNbyWa&&Yd)}8hAsX0(ZRf1>kt0b>KSvJ z+9`^>XJA*6yUSv=To$Vj5cVW53?PeVt03t!sbRp;NSXG{wkL-?{icXWM4shjA;DVx z5d|7DN(k-XhxSPUJ$A6qwdjWZ|8^CqFI^%bx#TipQWb^dhR@(|h-t~gb5l*)A+!Ez z3Np5RGTU+YgU8^net1xpwZbkL1_w3>2-IiUcm?kX5&{KZbG;pgMOYyRQ|(jq8`4z_rZ{SwDY&E`)If ziMkP?HEV3>kF#FCK1D)6$@iI7Fpd3Hk!Iwb;O0hDqRbU4s)$g+m9e|ajh^dmqt29E zCAO13@}F#nf>*~*zgZYIAK zeCY)R@l@mz3*JGQF}is4@Z>XKd5dF@SmQ|fdcN&?TWODoqMaRQ7As4LMTQTR?FXF` zdW#>fa*3rtM^Tl#t7EbX3XCtkfUu$6mU|DL_UGJ0U-RgjlvPx0>i=QJlADv$k})f+ zhq5H5jTYYrW>AY6&fM+GQI@FX+RAE(NlgvN1)lOmS^1$FFrgM$faIFZ%MkMey?Fyw zD8%i;!XCAB)$SRXFpIIj5-0i{UBTKp4Jcy#wcOeVU6C?{V86 z523=KKJGcYKf~7fv2v#T4GN0SjNO&?41qLu`4;|=X4>wQ6W*-p8RNapKaSlxKeDb? zTHnrkJ~lV+eUKZLDcH_AkgI}|N6*W9T?I-hs8SFqipYAboXr@c{+&42zaCL1l9TL>nU4-5{zsa9b-$>Ekcd5c_G@6Gli zmz|-ne1TSJ`siF~ZXd9BztH(~_f!_YG_*%Ov2s#M94J5pJ^+gJ9kJ*_&e|mOCp4=WVxYX6f=+KIw~IAY6{l<+U1|QQFwp$jzHI55;DB zANVv<9pg9s>zkc7;^xS{`%_~w#=jDkSTCmW= zzuhLr(I*dM-UmmVXX=^BkKWMCqZPC4WB$c!4O9NFg{A`7KR&nx)Qj7fG8a z%^H`pMB+{yFXj@v&Y~=bIcc~yRoZCex2tl<>m7XBUEEASSJ_(Wqx98KR_*`-#kIW^ z7yU$~d~G-~cXfjOvqrV^QdfL#A7=@uUEr`lQn5|(J@pL2!Z;-KG9nM1RIk4H4Z(t1 zbX4Z>`gIt!vffs{BD^q`&42u}1&>K>0D$jUrMDyQgHA;ZA}j&V2HkHx<41D#_YYcE z!1{>c;p7ap$m%!#ljVJM9wV$r@m(T;@G{XNgRBs6gKnB$d~GZ`cXpNma0ZhQMLlZ7 z43z$i0_bC@RR%_MB~Q;7o6i?!a60?6?_k54r(OehH8ovsl+;J#J@XGVU^8$3Hi2?8 z9rk8mWPD8G5Je5jiM%rs_)mul3D94}W0WA{43xp{7q+FBCq*@~KTW97MdZIKikzEmRdtp{<8rBDeSBOqW(BhYe!_y8Pi zO65$5AHHhvNKWA40|QZ({W+&?T=8gqwv@z992n7GzkM?T zK`se26q}lD>D8Kf7l3n&^nbQ2A`u#*NuP>TsQ*mH_!z5Qmzv%maY#!pMiE0UoNWp9 zlE1}o6~w_SI8RlptmzahO+ZPE3ph*bwWTD#y*O|-x4OyiL2b;-@8=92J$n_B>(@kg zI1#`<*$gKtaxxwl5D@TKMHjYKW|I(A#A}-#)>lSN5*R3@IyCNXP-5kJfsw{{>;9E1 z$;3GqFI~h)KX^dmbthMgF6-jO@O7L*zX$RF-x%P_CDsS*UcdV87<{zSxKI?~{sL?! zPfB)mIx--D5&PsxsiZt9m`?@J08VdaKErn^vm+xm!TFr6$WcE%lwkiHq{GJS9r;_l zR$&|i`Q#4(WI--ymQ+^097)0Vx{!s1Wxi_OpiLDt*Y4OWF?qsj&rQBg*#Shp;J9^* zuQk4Jp(z9MG<9daL0~AQZDkOjwYGAcVj6Rs>a%Be>tCg&w!A7B<5C95a7MIGtD=U; z(7}Qqdt+r1xTe`**7AZ>gmm=u>+j#SWT6~{^sr|WI|BjxeUrG2pllqso1sQJ4yi-? zy7H;akqqA_9P2Jvw7J2)U$lU&ZQ?CO18EKS5 z)x~vBL#)FAqsw*fIEr# ze{kOcUq0Ben6`q%Fw_?HR%VwSQj5t=fEaKPgzaLr-P{DSKnu!Q%IZNi|H-j{5ZzBS z2to-uFJ8@hOGl3>gYr{Iz=l=b+^*G=U7q@^dSCgb^9YD$ zcaOV6-DakOS8qXy#03hN3A2a@{+~DMFPU-2X6n^&Ba$uS{_c{*8d?;C!xXM)lvkfA zR<91Vf0iygIr_Gcd+bnKU0^71Q2U{@iW`srx7(bBj-2m|6Dj9@!VriXlc4%eM_&bN zUxXD8H8nSLIxoA?l(iyc<>^WNq5IGGaBiT+X&|UJ?L=zGEW%1a&0v@8LeB25j-_ zq7yh}1!6toPMEfXxZ@YdkzDQ(dy(nmeGYLJ@P02g5mtPaa%&Z3V-zDwMA9?c@66_|MEg?zK(7E3!sfMkc;WATtgiFMOfIy+RGGVVMTz5MvMI&D*aNE z90si+F_{=?NF$m0Zb8tgY07!y+f!gD+w&S8P@S4J9dtQfRY z|7Z|!1|A6O$7+Li0KRvLs|=85u1hZ@^u^hIgzkMgQPu03L_JeiuU_xs1>31Ed-@|XH{fnpq#Q(ge zUFF4J_W^sog*4V%R2v0aT4N^BWb~oI)YKGeBi?hp46&th6x2K4 zWl$WlcZm!m4!+*Ztt2Xq9N?2(q{@^91m;wG^2Dzwp;C^M^zzhe_M`^G%YSap<7d6_ z!gwzIblvciA{v5r$Q=|WQBNOD3iKKbx3D)gn^yP?>1O$^a-izeKf zc|~@aMX|8;@=>Of10@F~A_OBBu2 zd)hGZ@Lc_e5hKTt5dUi03YbY2q`90GW)>FrwWH|0=Zj9Y&i0dNk=?EtO?BcYq7V)! zhnk7MPK<&tqMa5d9NfJRJyB5DPeXU>Gi_^&q{&}fN{lyTW@e6s#$t|X*Dwae|&uW#{7#TjCAQc z$#nO9F6iM&$+Kx~h!TmS+H6C^!dl|TkTaE&;~FS760vv!1w?%Os|_ot$U!3WGFzPI z{?Z}%jU`hC8MFxL_W?~FD+>Z2u$7A#Ua*=9Oq=wZ!}TRabsc_z;TUyTgWKL3J+vC7 z^EmhzDO6xMz^kCR7*~a>w-@VQfldmr9z$KjERzHT1x-hTLd&W4STm#usEsis9=|+I z?0_*+XN4ZDR;7VfJbOY;NfMb0Qx)~&({Wu>hx z(yb2__1wjH8k@Z3O2}Ai8G=m8NlHT9owU_fnYd>Q=K@nG4xkpdNB`Pr9^FQI;I(DT z2MJC>4QG98TyW{09B3h11X(yMibgc3h*|^{m>YtNQ>F$%)+2gy7b!(`fIH7^59u?q zvf^VRfB&|WH=_}dl#;qt@3dt}h=gM9Q>#H6kj*DejMozq`>IrA4%GkwvIuFvwGBm? z(zdEfN}ls&Sd$d9trx{3fg*Fn@|iL>72_jeCo(5Twxt4U0eb-|)YlHdAP#D=ScLjrefFb0P8^x*A)!zA zPW}D+_MsM3msuY_M*YONttWi_`UvFU7S%@%+&(=-R)eL`9z<2{=dlPjn0U@soQDYm zI|0swI7fT~Auk3>mE7Dr=kgqTtV46-n3OO&|LTc{3hjg|5E4C8kx#kjK#>a#>9_f} zKyq0=gg|@*6FLTesY{PUPd5wAac4<8m+utyf9?Rb(i{ z_n+F)9yAlV1{TL(^~Yz*IARYwlTg|v4g~q?Ma}ULXddoe0+5t`i3?I~yKhY$gofCy zPqws3&P0A_mVxeq9|x7F2fpnm%aPQ{%|%dWonfp4W(wLU0S`Gb3-KI#<{fvBQ-p14 zAHihQF@iTAKR%Z?HK%z8!!+2h4LgSsx$5)f43FRQDz&PjLrI|4#OMIJF|l;ENt(~y z7y4uyUUX>H(}Q~EY$JEr3w?!=8dvcQUqKR*a_h}bjwG1201qufim-RiOO8k@0wxx2 z)R|G#7h~GHQP0M9M4rWg^sos{a%9x2;P_~N{YTE_{(N%3R6&?uVgNfa@*P(uAx)1< z;2<~Qh*jpZDqd%$ia1}i1^RHyqBAVP3d;t(1P>;QrW-<|qcfonKnhsn&Q~iuN(znS zFe&pQ7v^YSQVT0uRppKvh2p`Y6U3T5w%j}2vT|~*2F|PfF`$Q|hOsl|LHINQ98_Xn z0nqi7PfJ1j-Aj0BN0{~efTtnbF+1wdY}`i6&uzMX!)t1NiXM5`;`kOBHvjnOU}GvhGgQGw9-+AH% z3Zq9l(4Lpv`H;_1+7&(|G|@N)!=RUCL46O^$%AhZ;Ricy{Xvl_m;}%!P4c{J_++~m zKxN7rESL;vpJ^oLvmJ>7#r!^y5?obwjhmpRPGdzyJ)K(ntG832sf%rzV;e(CGRyJG z%g4-cCTKgMv=G@9b0}OSybre8;6m}~O9wk^h^zrNh#fT?D^J6(`=$SUtW5{efT87} z0NR_woO9J{5=5T`EeG3#Xz~M9Dmb%U3%zwLS?UG)at|Nk4XCNBe@iZ-DeqRg?ztXVw!~+g=a@~{{|Vw03g7?s57hhhFO%GfRU#?)XLp_c)zgndzzi$w z|LvbQA3jhS;1UqrLd5;^fl^=rGDJ+?{yiAjlcm_+vQLmAV7E>TO}m_^5l6D=$WNbC z4=0j&m)xIYar0)9^9yGY9M`)4Wdxb-15iNpRYqi}2KpqqQ2?@Fg7DmOYi)W9MHrP^ zZ{we2H~Es_pA)r~SkwZ>MMQ;u1hf#C2A||Y#=juEEBprUtHF=} z)||gwYA(l&c7vk^3prVa^Vee?$Od9u=`w)p8XKC3)xg2Q>1%r zTn2O#tnt`ge35%=(nra~g>Zn&WKg8GH(SYewhJH{w7K&t8H#&Xfbv&BmfoAsHZTMY z;(LjN@H_KuXv3+<)gPsUh4M_=KDfJODrKPiKf#z5c>S?V-qO}new~Vy7Rg7CxbxmZ z2Op)qv9b#$c}&f1-rY>;y}vaV&0{@!PGxEEBm{;yovB6Q1NkA;`~iw#1V#)-CfYJQ zc3b*^!T@=qwDsN@;i={Ny>S{_3Nt>KrkoPhb~b*49+T*CC78=}*l}KSIgQ57$8)R? zmY7t@r-)IifNB^A-7vvY_T>=*6&UUKvwx#q^^5?~@)t+*aY(LQ@$3J9NS>4@sT=3f zLN2U{Px~xP@|^RXe&}Q=_rXXT{8t{-L!=?Hh{e*XAY$9Y&`?z=Xy5#d^i|wDV@oak zs`JxADc1n>8W!04u=IECbg#D!S_|hC4&)C_MVfcV0-PYH)b61-eAZTkfBM#=&aa$h z&|Q+x5F|%>A0UPPWQ%TZe&HXi2?Lb|9NJ9~VlX$mo)FEbG!=K) z3Ac9HqxLB~!)Gv>#A;ANp#q!_aS{FtL^J`&nF+#Pc&+$3eH-bJDHZ5oY@95ikIvL0 z#-r*G3(E!JGQwklw-)*U;_v3lo+U&zPklx#t_9Rzk4V)CV_%tH8wtxW+FhhZm zsAE4V$n24RAiE`?Kc3HG^v2?$kqH`P=2E`Q%9Tc~H&0E7F!-zra5HldJWv~MA|fYE zmbF}N>o@Y~pcRcsXOY*1(*dMw%iKF%&?i4>-RSS+-l2O^gOr;lzKaw78tq2ULC;2J zRPcQdZ~FQ9<%FfCnurKb->S7=8SsY|c%v$iJjZ$8R;wBGbOY#HzP}oAR-0gS6Ag^b z@GylWNzjUBzJ%UzyprzCPWR%1fl;>+4R%_9Hni_D4is@>U}gbOI=Cbw`Uh(1;So5kVmb zLq;dLR*8xcR2T+4JO$2PsJODAKj>`yYgg%WZp)E2UtY)M|rh!lKxl3!4 za~p=FkfB|0xSTu~-e>|4!nD0V+JB-}p#2`19FT|fWPc)|v>qLezQxCPfdt|}1T(wo z#sZ8E%zZw3ALettZqw17fzEEjaT&RaxY_d5ddzch)ij{>K0SX9!R~TG@}NX0puK$w zs@OV3hSv8L*l|yRc4>Ne(f4)S`puQq zjgs)_E*fKp}XWQBQuYe7m4{K#lDLJ$REI|TAAv7dvok3GDeg-p4SSZNzrgJUyO;?w5VbXhcmx1T$11MM_ zcxRtu0aKtAP6CJ;5*0Pq05krtnfLX61n-JtalngFqZQ(DOE$IsNJeETymk-jh6eL7 zK4ivq7N*uf_Txu7E{8pd%4)vXd0s;QS%4CX>ya|yzs7D(vL41c<`De9wEB0JbZOwbk!18px-#nna7-ThkZC+9DkoVDAVZp{p138VuFl>z&cI#)WnGK0sz8g9}#T&NXVWkV3g z8APHV$?27%TlC>+liiPYcm8D z(5rRVQVaQ;x87ejo|D~CKK9stoR<%bmdEOqtf?sj%qo7g8d3rsUbS7%b-b%?cRVT- zR09n?&r6`Zsv$+YZ%Ht%p~7V3*`lL|_i!&W;Ds0y>xR(TJXXDm6Qa5MS#(kVkGN_L zz35DJ9Sv&d0p<3YnVfRPtF583r<7SAoyNKS`n&4A*xeJ)B9@^kw>;+o?IZMV+cwL~ z4pXn&?0WTILzHLSsF|Wx+uPcbE4Dj(N#v9Gf-8Rg(xPNyVsofU?eDMoZ9LPvv|SrX zI?4JrT|-Yya&K>s=h=_2p%SMK5ibh&P20p|m&B+v&&~2Y&q|f@4`fzWRx~2AGTGtu z+$<>-T$cA}g9!IhhcLgD8PlqFq>6?%^TsFgi|vmtn%=!$M7N(D)hHq1PWkA4Y@Qmk zN_{Ff*(S2~u$syztm^m~>O!XRS%q==+Q9V_6puYCoD@;u zlU!4$FPs5H8Wpdj`aeuH2AgiQl-$1hsY$DONMa>h1K z=dsMQ!S$aKcipEouU?cx2P7o;;mt^6-aH$AO(Pard->ed%ncdhb5pkcpKEBm51i&D zUaK-Eak1f7>9(cF)+V&1$Uan%AZ9Q(H}6X3!h8RuBlxvNG<*MMUS|r-Ak8Q+FlFC3 z6n`{2#$~Z{RqSeMmXc(qGQC%2S63H`*W?Pa@cTV9dO7oIiTp9i$#7Fe#VKdig2gtC z@cE55h!1tA@J?Byl9ITr+8E*=-Jq{D4$mx#K7+cYW9p2+3AagO4E=Vu0(R%>^{fm!`_&sff!e!|duRp#DTcl>W$ zRcq}|NSI`w%J@+tLCF|#={{o|CQFw^ZC0VON=+JjviNe)@%qg5J-(Y&~g?kdev!cyC-*)Z(;;C6)dbFI||dz*&m}2kg#!U#Xt=c`Q$@ z&wps3_yxH`75;s<^h68m@{?jhFwJhR#6b`08XpBX(AfHV&ex?U%n9)v^AYRw``ZhR zhgIBelUYgxna;1pn;N43`}cLFERMcE3uxr0zVzkbEm-iAA)g9Uw9Hep)|xp8)D`g$ z68X-v-izOlxfz2ZrP&iPh2R|_x5DRs{(Ky}tB|XD7q&XdZV-}?aOOU-n3y^D36b9o z+_e|F?Ad^?%<&vDvwLzdmay*nY>4mWd%3x(S=AudHx9oZr3(8(nh&nM2#!A;=vZ9N z{w;razgh9vZ9kq!@9|Jnj%qostx~3PpHT6wj*dGJ4eP)`)VlT@n7uumCEys#d7kkq zWr*EkWQ@|-hQPdI@GX$2W1?nPaI24_Hp<7_)9~rj#>qDi0v&j)Mp1H!nQn(zJm2hO zUJ>kmzB|wQ&h+HF;^LPCleWK{KV7tZQCdnuOCKq%u=M#AJnm&`YSZMyOm!|eYPY6xWdF$4;D>bKOHQ8n(P?#NSWg^9ox%AJa_OBXiSEshLd$=={#n-dhz^P7}#5}fP(69 zfgB`REK8|{VwQB|v)a4@@#Wd*Y^A;L=bcwV9+`!&+h}TQ|M<}$$eX7@&weZvO)Ea7 zx71fdd5OI1?UVNxYvszGsUvuwl%#c-@Vby1^dWBAt!IbmUf6Fz7=KF9j76lJR2yOf%ihSk*ifBxV# zzp5%|zW2X-@5}Ar;83}}WQ~4ruHcPJBN*^TLae>IGIqD|$B)=o$KAQgbrklPA$K_7 zHSG-agl457Spu`#Pj60s{%pk+icd$Zr;ccSU&h8leSCT?*&sZtE1egnRK=Pfy}3sm z5S`M6^(6+A2furlW&iW%<>TE32_&)d0lvJJax^$RN-5gBU<4VkF+-U-!Her=Jm9WZh|N*5WVF7&n;IZ|ruYeYxN)l+ZTjr2 z-Ru|poujoIp%t-il&h<1>$_Humeh|ed7d2?K=b(N>Ld+h-2Bl`Mx?8&HeMIuXM*m` zerIur9N9PMY?~Y3FuXYJbMC^va>)btV|I6UcdrT?l9Y<&pFOt|9@V3WA1&SUJ!V(R zn6p1_x}}&Z(mX{k<4yukvK-4I_38Fj&V9be^j~i(E?v}9I)Xi_FT2Nn3w!5BAGaF` zMcGC126LXBr}xrEd5epS!P-i}|5zJtcZJkB^0$?c@k}9!HyasjAsm*oyuz#MQXpHJ z75Vh19uXy_6`DM=_shWVYe@Q@>()8Mw9~=`0r^%^i$j}|Z;B;}(bCdw65pitZ?Ln! zK%q2aZJ0Li!m|s?@zH2~EZ{Oz)x-4dQHoXgSVE(-HmPwzFpsPtmz0vNYI+ zMiWl0++sZb+S;ky(x%TOO)lf%ae*UqKxq2iz>jzK5-?BlhsnYd+>BS?1gVj=_jGm7 zaB-C|OeoHo@jN5dw!U6lSGR8cyq(TJaQW> zh^(ulUhC}3c4ru`>}U>7GBr1k!K~kmrZL}G@1+kT*En@pdxaa)>brTqyR?ok!_VvXh(oXug4j?Oi4BW@G zz%9i%r88qv$8{YX9bizk^)2fBzWDLIO!YVF`O3FhF9#?+^zji>N}qjsaIiKzJ8NvM zN0r$aqMg}P9spDuf2!~8&{9W7$1NuQL_a?ZR7y%_@uz$6M7Zw0oBn_O-kHRVjGj0q z!P(q+3E$0{e^n|U@xx-C~7RchIt-H1kGqwkWup6TC{#GW7WG$lp&4A^;;i;@bBKG z^!?ld@VZE|+MlPEHdlr-JQNOX*{x{&d7V+iR-Q%#o zeoA-2|1yxV&TIJ6vkupbnhiNm-G%>1UH|j@HIvrI<#;Q>L((u)BY*3Ay@zzg2@V=e3wlv)YJWOG#i%e=m5+V`usL z+}xZ<&tLD!2qHL2{ivudUnsqV5_ozc^8w@f#G)BQ-n{#N*dmAVe^@t`<=guiZA9p3;CVavF^vj!xowvXKt0gA>N1Og_>F~d8x-?RXz{jS@Ul#JF;M;Qi zD)f}AilV?|`rY-2i%Fkd^AnA%^-|~mvO0&!Upo$g9n-~ixLyA1jYWUm@9$SddTQ#K z(A%Q!f4};d;qf8XNuaXE4k49vYE?K&^3mm-|E4$FlFJQ7 z&CV?%^2gbcdjGL?Hd;LWE^G7{^((wqZr_2=-PaQ~+}L_bv(zxzw9sequsSWG#*;LN zi24uYKuo{CBz`tj_(J~#2Z!h=Vs^Oa;I?{KYO_q#N3$ixE4H_8Vyt@Oi}7yes4?%* z3UndyT(Lt5hK`}y+q)*$p#N|^=jNxq52Ug2{X7>42pM@y1q#j{RXN=W8?FSYVS>QX z(NRC)fziWZ?BnU_!p({5$WMe@?F_egBiVFZuh7sm_SJgTidd0|IN-9gn{kEKXPNxU zbJmR8uCtF>Z#bX>YAwLbuZG zIzR}<#NQqL`sPhH)%>r&zfXyH#6Rpo`(G1hM}?Hm3zGVfJv3wyf8ZDV_S46v*H$a}5V|PcTBktRo*jTW@MvHcA*YW#kwn^&6e0_{Udy37_i*J?%9weMH_G|H zhbow$h1|#>D(ZhrPQ&iF$&7(5nfHysBu!;yeCzD=dzR%9&p$|*pt8zG^qofk10;!x zZ~kq?{x3JFgnhz*-abWtz&lA2kHaDQu?IXgike8KXlQ16x#jg~Z`|jZ_^#5((xybxGK$1xF z2jBFy_KD_@OxmJhp8gE!*BVZn<=czy1Fa0g2V7P|wsWd(=U|!+gQ`T@3px?0VG!i%1vrvFX`~P`m4^EAEd}FL*ugT zU0j+f9@1afT8k`mw*SY=Hs`wyXR@p%(;EV$3*H(`rb8w^AGfuI|B6+waqm)M%qPpi z04SV)PCp@w3r{R8hJyhr5D6E*_jSBLv?>D`|G4XK!^}RM(X8;{29x1Y(t?^#4PC#q zwL$A?h#V03pgO{*_#XUO{rDRGg=&{ICDEd4sB*4`_FBHv$Y`AXHD7Ag&Ik!J_Jmct ztyNBAxXzKpSHv4W%vQ6$v(uERt)?%tqCJo#WOl}+U1>^p_Pq8!e>;5iM>(;^B@Kej%c-fe!IU456k7gKcIlcKu&u2=otlkD4an=#4p=@&&_?% zT>dvMIGQ}WesvSTvjChsMOhf|hU+4gw6~r=e~D0)H1_n>`)OF8O}^Z+e7Lil=w+39UW42MCDQFhD4HGT*o`;a2I852eGXGw`o1u|K+_+(#%~M1 zT;#uX(N7|-vl0dWE4uaev5t9;?OX=$d1>;g*G2@~b!C z^H8Rv5mf+iX%xI7xCAuPZOt+f*ZGsc2Ml8q(z>e&bcP}6WSlR52L(M{kAd+<{0QV! zhTv!_Gqhh3B*D&4cSvq8y))~L{RPK!CA87daK8&~`H29klOV#?o+Hm)JVFA*XElMt>DjeMnrInqv4NGBNNNs@kGd09}iOoP|qhzg3n({ ze5;<1OG)det`h`kT0C`7APe-X&*GaItR4W3%gN4nb+Sgu&hnZ@rn4-*G<}lRhY{f$4g4m6u(Y(G?=OvpHFZkuFb<$Lyy$p)YjVAL zNAKJPEElNP4vvm+EcCZ{2_EX}i^2NO1^c|vw(cY+d0_AGy%SXZNabAb5UZBVnfEpF;L_%Ex6o8s&3%D0&c%8 z!0CabT#Q@qvxtFOqvgGvGITa$)&^~66|g)T(wXY^NBau$@Oijqzd*T?(mrB}yZDq= zR%Gn@-{#naIki5`Qu++nhkrgL&ZtBM-Fx=NH%6ck-2?|_6yS*fA_&7Z($O|)5beK5 zEI6SizR}y-#(mfAOf-$zT_GodfqV^<_Gr%}kzaBV()3q=FBD2jahvR%5y?|~@7i-? zRs4&g>H4|5G-6Gr1IYv*bn9rKUu7VV``+}Sa9c!*yt2Fd9w<>rc_>+Q;t#Ob^?$YZ zol#A8+q#xd{R9<75TsZjbOZ&Yh9W&Q>7WRRNL708A}YOwCP)!N3oU{m5V|z!U8IRL z5s(gofNv6_El+PY&$u=7PYDp=x6?FuXK&sz{lCxc)dV|4x~!5b$WV zNDs7aw95r7-%GR|XL)%!r0Pg)!V46K$g3~i*WcGOejCfB6(u@JW(DKSQtHrdSU+hh z+O7{@PC_f%0v)L)!61}ge92@jnj27{{Kk%W8E9l7L!>L&+A*I+{mKrm_(j&8|KPg) zH^%$cVWaa|>r({>y?J;tKq&GG#QI65aaGMf>ljJwJqrh46g2sGO}T5KoVstFn%wt0 z1xxPA)C406)XxyIS4~JAc$+EqER^gj+uRvQZY+?Mo|<(bd=SF49NhJCcVT?kuc`zm zDly%;6FyXKbu>d!7KtQ z$Ar!gyyUSu1ACxSp^<>>Ew?@QRw;!>c7#7a0$O2-gTst3`MtQrFMn24qZPsN{w#9P2Hu7_q-}x~Y z=98-ey>{5LHusdtJDZUwvia}R9p`#IKi5$C9p9*X|NaQZ$mptw_oyN;QGiBzEa`<@ zmX7EPm*7ASfIMvnnpTU(ISL9i(xi1Ji-v121OzMIK}|J3xBazqdNh}~%G??|z))_- zcn%POXN?{0|72XqT5xjlo|~H_caIf+0c;=PxY+B9gr_hA7PkJ_?raFDBnRCe>OIEy zSd|PjE;hDmGZ3hiJGct%^h-ezkwS}l1(cVEOP2EP612Jr5o%JAux?KcDp2ZHt#NR2 zaz+4@i}k5^Q8Ni0d**45$c@FZB>u$;2fz`MvZrNu4#XAh@-=-2IBH;X%8@`>oaxZo zfG7K(@sh;G$2aO25?&joTneEPqQ)#I-$FvAbzoucWBz#s4vx-5A^+QqHXxmapsu_= z=CO6DcCDM-A9cE+0r)Qa6hb&sL0~Kqlx4-q<&ia|Ti30BC{N${#^IO!G9!PLEa@v_|g@6lE|ncpbua2W<>nw}=3;m-@7o@``g*UGEyXfOnGfFS6Tz}6F;=W5@YbqPA76QMz2 zjB8t2%c);6I{I5}y&BbG>);?mMH`OdFhkyNY|!n*~__gnQh$y~&f8 zMP_3+5NGK?<1EdN$L$3nxD3ULG zSq8Y4E)vqhLjZoIJ7_ND-7EYHG?H%vI1Ul#C8L&J%6oyt7oee4%7YN}_r;qOQKO^d zu;2S8B}X)Z%dn=(wC6u%a>Nm&HNYC63r(;+~6Gs?h6Kskgu&-ky6(2MEa&=GmXisphOTDIBAkaWzHtkO%t zq~N%;l}8h8Ks{vx294o}y$_NTguY30rzqzg;!_T_!IeBc(x^EN7Non!w#IP4S$>{r zIAailnp&dF%77MIiqo)|8>jo<^EpdUN)h(G(BT!SS#U2B(hS&;{b~$(5snwSy02YH zzJ{cRC@@cucN2Ntt1bZ#2Bh=eqhrUn)%xKTIofz7ciH1Nh=LCs?V!!TX|k8cyOLsH z@a3T-9LwrTWMpBXf;cZWUs6;%P1`S(`lbO?I|pFm2MOcSNc3H9+~ zV_;9?Id}s2xSNkO5wZ=0j$nW?et=K2hf8vJAAFJkVlFi}bT$sh0w#-R(`u#{$PX?k z1EU8`>ZR2|o7aO27e(Cv!j^3>puH008iv-lOU;7>fjWV~2dr`x&N=ZfF7|L->c0d! zg%YHu=3Mjt^Cc~v-bx@z!Ln+GoE;^5dSM-J{;~fAh(MV4q0*=r_83I90s)R%pKhkD z;tB?Xkx{RyraEWV1qa-iVzEoF-)MGM=$s7;%V5iasG}IjQ1n0vocCd3VTl{}R?P5F zWr4N%a(nlt6i)jbRf>e#zPIuXHjSo8K(MY6$-WIO_)`Fu;dHCFHV0Qh;mcM{rArci zCu3nEW4*KtVp!(g5_S}76!rT~&UkoupfDI0*LY{Eqin4(6cEn{XAij@DtbKli{$JN z_K%{N78h+@2=B{31j}<7R6Yeq#FyEsKtYe>%1Sh7gNzp6`wj*6wgv(FH3c-1O>295 z2s9}XFs9P#cy%`j&4zE2Fi~pNm896{Xie1Sg2y?#AE4~nFhgHMqIj&zO``#Vh*;);aEUM8ph8jw((RS&;nu6GP1#}`< z6>zTV*!K~i0N@$eHCg3=Ra3irC-R?sQv>Gfwc%-b`N4g;ayr1*Mq#n7zzYejt=pjh zg-NIr4Cqn7h~BaXO&K!hYs}Q~L=l(6fW_k?`*U?p{C_e<@@0XLa3BLIfjNtYKO2_P zf1bkfWZUx@>VdHGkmnQltU-Bu+5m>K{Spdo5+%@KfFP7Ve&)?^OT+(tiD7WuKjaB=hRzDJ=Ic7tYm~pK6+QHK*_9UP?$}0#7 zsz$3@G#;e%1fX7IKonD45Kkqt5xgt0nRO%pm zJLj(&YWMCWreUqeZ}C_)q4$;CccezyD|qa;B_8tVzIA4V9R=_WZrDl~0&(>^8%mdY zG>GZ)*eDIvTrK(HVIDN$LE{>V`Ewd8I7nnDxee&f19N_XJE6X4CO2Nh<|Gy&(Zsp_ z0?`h25c~8Y1oz{S>Q}R46)q`pg+=PB(LLr5`(o)mYgSJH|44qqAB!f$6UaEU^Q8!b z=k55(eQGih$pFm4a{U93)7&Z-(78^GF zOY;&FSA)i&8H`e;jGD7EO%S_5r64mf$(#m!{w8fYye^-~Q3zq}eeZ9mxtRTH$6DSk zRt&xK66gYVj~F-Z&3`w16k4DS>};ZuJiIg}eBN!J9K1!6NYk<5vsVW^Ft{($pM{=Y zZe0{7Q zJ|fqVoZeO8Fe?CbfyCs!kAdGF$+;TH${xq^gk`vDfpgFA#3oLM0K0u{Z1=X=yhk?; zfuo1h$+05EH z3^OX@F?!Z&YTv&JjQ z29onXM0++Evgx-TZt**Dks=22`iMQ(!n5h&5?!!3)lLtlk9g*^3dYJ7J)S#)09-V< zZcrHtLf64OD(UABjhFFO@-NQ2MRc^UCcy;RJpE(wA=spi*RF^j#lWYtP3h8r?O!U{ zW$UvSl-;jRkWSDK#g;8${zZC-u>}hFfZD1Y+OD^Sg|g~7(MR8!Dlzc6QbX}|ULNX7V`?ibWK)4p z13c2*XfP`Sh8u}Bw*!NX@1LK~vQdy2#NdqCP+$;ZHS9bVH3C)}J<8U&DS(E_=$2H- z&q|I&*Jq_VO@(||6Gf^o1_Pn9L!WdR5vAx3FY`GXbO`id6(o?r)P}S!?++}9m%OjJhYW#jqYk{qQT(R&w!ULu=|S{qQ{!06S`iTZ--+eIwdW_Zk35W{YJQ}}qG#{j!LtlQV>LutPs_RI|A;G$#(Ng3Gd z-+l{ly#ol2dlK8hcBl6em^xxOdMHdlRDu@a%q6rW$bF!{M}TLP0mK|BfA1y$>X2Sv zLXq`8(C=CR7Y0IzlTvZ_!GXC-mIHZ6kp>SQC_2dFf{y)4cUSsYRWNFP{^q;KV}an= zw2Wyz^=3jJ_Ia&jGTLS|s7?LL;NnS;<_+NHeWaJV%46auV!^>Yg;$NWnxqQW1Z5xDn5cEJPb9_r2PFmAEYnfJ5~RD zd;DXdR_Mg+LlP6|l)%RHlfz|c@5RG!U-rSlBLk6pbCC;_2MR6x12^!zt!)-*$ycHm zu2{@3)bAhlhg(KG0u(XOx!N#*r{E`PwIoh^LVx^xpq_~iYL@$+?4P*GC}ILr#5prY zHdfcBs6;yN(US18?Z3c=tL)|VQoxJ``>E_N6qsCDU>aewx)B^8-}KTh3S^}V&~*T? z0ZLz!UGL8Wnh4Oh0NeWOXqDSDp=$Q*pCV&3X;ofPaaG8hfiCL%_hv+Eo>(kLrVixx z|M+9DRh+Zq$AbS2-O@7AtiKx&d|Lwi%>9G@ZVs7O149*GXSeC3-MB%03dm?GDD6DF#u*Y2c^8hbufFLdARvj-G zR9B`7ya0;4?#V``tx~8jqM?cD3Lcg|G}CC$?Z2bIJc6hOK>hx+o36~s(Ygomf-L$d z2sxGKDKr0Nz=5L(1d_s&58MK+g;Nu!=*X4gR2GpQ_u$8YG<*b--7u!6{IGvU3e-$K zi_r*(*B?tF0brhnz8nhTGilhs)0iIbI5(OM;Me5Wiw-Ea6C({L0to~%7>%C~dX+%B z1p4;_*cAfMK5*JZGgu{Ewd__;?v*XnpuP8=tEL3Phj{}W{3H!nxETTtqYmTj-_gJ`uOp zO+zCQxeOTJIuIgG!pR*}t1H<-(<3qCJ=J4oXt%XzK>e3Y38&i)JPmzz6P>Ze$QMytL2L4JY;vFPWn7Oas7a5w-V z6t*bh92FDu97sFFxH&O71G|)NFIR|PU&S*HO`M#-X(~IR&+k;!jVaL!{ zEg_4xZuZy57Wg06ne(Y=dXy-5E=)F`ftQ1J zV)0+S*QhiIjN|3^WI^Pi+nJ+6T!V^R3lhTCE+S z^9?N;JA3N~{jX{~6swmXH3xUG3kc*$FsQvA@j>5e8Jo!=5C)Y6F3bgUhs{&n7R~24 zPS#J>G?uOUq3@B0Pp*E&migC^6VZ{8sXlvF&yUoT5LCY;S1hZV40!*lPi#yOv&vCP z5zAlXUhIY6BFStdon^glv(&ZmVNEN6-}s$%l}V*!YJ~UZWLwUDnrx#C&av9M%sbtE z>vINx&IRAytZbL%V6FXDgPZ*P^OBUImon5C2|WqTgLAbt)mh)abN<@j+NJDh(boTE zclZg%As>ejJ4b%6U4t}b+LUhISEZIynX?qILz-}jD=7qjwLn%Rek zOl6M+aF9}#SvJ`ItR_5J-X+4-uQp$wKKSo}$XT~FFGFkA^a9U~o|KIqIUn`c%NbsL zp025xqYhb36ye%(|I$E=J^jfC`RIMkZPru zqTN`{?zK|qKR;Fx_s+`et~ke?D|*)w4bwI@_~1CeoCk(6p5r=pJ)3v0*GLyEup=I@BvRnPNGZ~-OgX`}JG6^o8i_agfX2jQah*x41FLXss36g|V;dMhp*{Ukn z1#JILLoZne;{4cB0-u_V zRuxW_H9bt$z@x;OsqN+(F?V1osO~6b*@G1cnD&8AC;el+eSMBxHJ;uTN2-@|ZNJ@* zX-~*ZOH-S7vXi9$ACCYw7MCd6%7wXh8Zk?gM;P&>qu@HeZ!E4zVBe93LDV;??4Fj~+%Az+x)??)tXyu{5Ef zgw4JaP9ynK1J>V_8!aCz&CMj$s3AitYv59%K-HlJ_~}TK;C#ppSzpL;xtBepM+}EE zh8z?rk#B3#rxDa_Pf0ECP8XjUjcu{d*DcpZ7Op1IHQD_~yT%n!QDtA-Pg=Fqa%Q~d zUk-I7{EXnYaWja~+NCUe)GJ-3wHXHKN0^4!v{-E92eyFbhSFj|wPo46sy9-`O(EKH z5P*QJG97--tdCHxco{yII+6SEBOfJYs5&9|{JiLWn{as6w1FY)xNflg{UdZFL1da^*g`_{6&)o?8L z=!yN;2?=+VlhN=yLZZd)%JGM4Ep~$rdBSfRs#_}DWTWX&Gqbb|XX9m*ij*e~5AzHD(Y%G$YIlx{1_N|A;EjbI+ET^wN39pb z>*meo=#-Q!f{4S{kRwghN#Vo8#+KF=YF(A+i&NPm#l^)C6i%g0nhMHzW*MB&ypr^w|45SZzpCPGr!7jV4iQd)^Y_@lTU4nK=igyL` zT+x}N@WrCzsC0B7YSz}*9r>H6f8Bggc+W{-&}mb7Mf5h@83&|c;-p|03CY?(3?$UO zJ+wY+vmHfvbAOWe{mla3U5d1#>3JW1y;A$Y9Or@s7 z!^`W)1(QOQ68z47sZGm8Xk(Ry=+WvpCSZbHN`jKGFkj`jyiihQ^R0D4YR2tc>MjT- zc#dSM$XS{Gn2ilbCh}v#aju*?W2thcnVn3?+InxKGs{8eJdkyp+bIi)8m9krm#}N^ z+dX6u{X;^Q@gnp)+vmqbO^8dccraTk@tiOypX;_Zhd&EA&a9-aRR$dT)IkY9e|>Z* z#XSzrn8xAB+STQ@#zgM;oj85=$#PiR&Q_nO%XsRd2o|Y|&QRFCcE&Ru`qpmpYj+O1 zvhE-B7_0F}%?ik+4zum#hXkz*&mG!Jf%Oa(`7#&{C1LuX*hC=E4Spmt^NfL`2NGR0@#OyB$G7G+>M={7$;SG>UQ#JCQ7jQaBDD#tqq-a*p6&u#n< zc9v_tfuop)&-Td^n^(l0DPh+YuMBmz&80ELxXtA(d6fR4FU4Lm@U!0Vuhu2;+@Gua zhZ+A|)E<%jbMfR@$e)Wtq|}H1TwS<+pqth3f;&0?`&=Dx4{y1wy=FJZ6Ll<;w=anR7v@Ku!MU!$Sj4M#(} z!*ve}{HD-O=Mx&5fr*Oz(>ES>wwvyo3^{qB{6jkgY%S9exABwPqia9k2EVym?Van1wQ}F8I(otv z-|3kD`HEkng8lnz{C|J^dn^8TAN~~)|GN+WyAQDce*}RiGjlvvp4xSLF--YH!MEqj zi3=rJ9Q;WC#E;&;5KCP~mvHWvXI$aRYM&VM$+qPfQrS<|CHX}PkIT-atB>#D>s%)$ zbaeV$$o$fu5^WlFbwbw5TAy>AT9%VCNi*KH{>-(Z70%Z7_Qkce7yD*y_rC2hR@%xw zE&A(;{LTAA2raSqNtvxXYeTm9-|KbTk*KfteDoSYbmtD?Yr#F}=2$^gULJP5G#eXR zDThF~VJ(Ze=YdcPkKyV;Qo-`^{@1_!pQ1mO>RgJEZcf18isMkHFAJBw5$lQ0B=$MI zR=+x#qT(^Gmy1wZqKfiYd#4 z|GXsa{(8iu2S#e(B>r#IO~GgM-K5iVwt`~FPSEniDWOAFTp zSEdv zZ*Mp*q$Me>(uAN=_?u%)k`g{=59=;ul$@P$`d@#Gl`YWr2?48!uX+9Y^}DH0*j59I zOI*iWBankjN2c4vj11LRneDksZ*rp|NhsVSR=KkmAqKSlLp7S-A@xGALRP-c=U#Tz zMI9X-=^2b{Y{6A@UQQ==LjA8BjO^JPZpn3LU~%RXWv-G)=Ep-JoW~4&AA{zuKa{`m z_$Kv2CS3`lS8BseI+qJ}|Hx4taZa zNx>=!i&1Z!6idv_Wi?=9oM$5U7og~(9gvny{C15EOAg*T7*MJChG_m zUJ*w;S+U#B;4i=Qc(?A5eUxKs3Hj4)x1)5aA)bE9ertb!pS{)_dHzX$M;2BfCHfa{ zowqAX!RGILjy(0-=vr`z1)N>=to{QsTc;%QzVy9Snv%FUH>GXM&K?%0FF1rrR$W$y zL~)U8`(MGbb9{fz3u4vrMgIBo#=v?%e#M~1Iq9csU}rZuk6yFO03iryOzoc_K@Vka zK5B8NfeeYTh};@;d{zBaus=&Wlv80xwlgoAJ4byNx%`HvrTT|wkH}Dge?t~xD9l)W zutHbTVH(z!RAqM^;%Xc`TCPS`0n>SR5!kh$P$Vw%sN<5X^nEaYVqEIjXs&NP|r$o^ffL?Ivk zx4k5!&AlAH?!p^w|71|-hTrAG&rf0frvg9qlS_3yt%T15tB5+!1U*-p1l{(i?9R0w zEoNq_mM8MgxdA~&>=GAJsVnVKI(pSR&$amdnlCfNpz*y&n=sKDCnhXt{rb8K9<5Mf z?;nJL;mkU_t(+@Q$PPTyDy(jqdJjDSTX@&HY8DiowL83TXHh%uNuwC^Q z!$Jc(Mb%z~Y27J;87K$H(+AKjtWk1Ag3NUmhoraXxbn1N%X~5-A{`3tg`X3rf`s8! zR#SV5EDET^MCPhDti?r+6?F(ANWnP{ZM94=xLlstPpiaa4KOnouY|#tAEI(y`s&JO z<4-9ap?3bqHnWAjg&Z#Z`ZP}M;>B-ZS4haLTj;oTf1)dq@ft<$n;WmiZN9D1x3PKL zlUxGn#%5TAf$~ATrc>&S4~14M$0#=8m&)j7HwRybMay((Z;29v1d6`c&odVDX9_V> zh=`q;MiBS(8+togmDI^3AKB1NW=vLQ{q^L=C*$TC&vvwUEyb8%>6G@BZE% zqo8U!KSfP78Rr-gsHI2m9!wK$B^*C87|#Z$?^EeVs&PrU_>IB;BKVNP?4ef0J>+Bd zRpyDV(j^WtukE`V8ns$4(}IP*dhCBTKoGTzrB|2wb~wi;yTu8GIqZ zUGwiW_FQONkVnqMuozMP$d?ato)-T!Lybvfan0%mZ7!Y`Qn_3P_$Exzc^Oi7A# zkwSLRevx2ZoT&n?xock%zdA*!iD z0l|l|%e%dBk+}0i$UKaS!S7?OIE^ns`Saw5YOVb*Jv=;O$&(&yF@k5)1l0m(5usfufuvj{#8FY+paWE@k1**=r`F5es2rmiz1+8z-`~P-2jA0{ zdTcmPcj=SPb`(|`^Ry9Pp4hb&!r>9HrDjq4w!*9PL*kQ_HF&AxfEMk3b3{uQ4*)Z% z4Nm+>9#HlmZ;SMTL9E1UWo6NCM~w%vWNl7IL<+43n|2!G&Q*|=AFK^1ji+Rrx7SPS zLtAlq2|5hvByTva-_|%QOixVhsq_@qf#)SM1E%eVL0L?Bx|I@mN_G7=$ozIt8mvFIXvb?YAojX zoiAU%#-q?Bh}9+y$0(X$79OLYs#DsiLa&taheL3`9y#y&#Cc>CwKU;=hUS+Qvn$w-atCTA6h2M5DcLOnb zen@e4p3;�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%0D!`SK?hshr3L$*aagIV( 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ⅈ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 W40`n<#;wFsQ&-hI^jzhP|9=3~zrafX literal 22004 zcmeFXbx@tbw=I|;0TKvq!4DoBLU4%S8X&maAp|G5hTv|&EkQz%;O_43?(P!&0Mq9u z_s*L)_r0m9nm?xMQeUMG=j$}xySJ>hb_acsm%w=X;_0JDk1(VpMU@^sLJEHL=rJ?u z6Yx!eh5FA&k5uZUMBjdNe!Sm;qCMf)+O@*w6&@LEre1SeTd|QRX_rOLJ%UG=L|(3qZlBC_=eC+Rba^P8Jf17RLjs%gxyB(hfG%$a?6kDwGN-D zR&#N1JPj`C-Dd83bmh~9+x&d>BxPl@)Hy_?<6nw}?W*MJHIYXkJ)wlSZ1u4niOkf?npc$H{^76}r$K`OnhLLn^ZOk_aBO~!#c*HgAtcceW<~Eoj z)K%1^+cDJ@fK!E^(<*Y9;rJ)%m1p^5rv()P>?qh~?9SIfN%C3zssP3@sO`xrZHi*9 zAu?2!L}4N?_5_cZm^hS7B<#`Qi|S){y=xxP6)-QD{8S7sIqm58Pv3qN`j zkI{X|mZn|bEY=_4RBa>+E89PLecCqf!ggl$_j8J3X_kmGT|;x*W6Td&9S7m2+sA}9 zlk#)o!FY_J>uGF_u%yNnpA)DPk62@oC6qn6rxWZTVWLMS>`z2Y3XG}7WJ9Y+sX-I}^u5vE8V&t}w3Wv7Wd6q@Rb)nIFxkLF zzM8F{(4x5Rr=?s0kvsd~`P0#UDfMLG;2by3a!iSE@-BP&> zjNqi~+P3uE#hUKlce6Mxx}cCuHA(2!+V61wnYG`XtuS9Fph$n`UhTr|DHZ}2I#*B6 zM+u33#>vIylC@&wesLYYUde4PLMgl`U$;EDUP&gfwmpAtR;VLG&GSdM%v<5oOMdS4 z+x6K0;)P^O37;dAR;5*!%H7Rh8Xhu%dm$DrhxOiAQM<88OM&U*x6BHT7%j?oS8GGe z8kMAK=eskh@BE%p)tGjWSbw?L=Mxlb5*D(pIuyD&y-7ZbOpcC@b~&%$apqlqEFI4( zW|b`Dgq_(b3!$d=ng5=a)ZjwMTWstexhD`v&GKr8j37*sYjkX^he^-t zj0J<-Qpzfc+Z1~!BI%7TJf=QP^9I(ln-4qq@?6>6$K64+9Mi8R3NN@1P8_F(TI%aFQtTrq6_w11&u|0Tbt4=vMYOv zS{@x2C$1Z!Nj$Kg%N9jKZgxc!&#Ff!;(oh8#qZ?t0y}EnkP@;A9UwQC@a*mGUM^83 zv(2IHi#zK{^H`7X@0Z=>`7C9nUTVRYzCR=3+0);j<(U~5xW#Yxmq-M5g`2h?qi|9B z3K9+bSmgOdumgBx(47ql&&P(z|g z&_zY(w?}^cIYDZI*+p$IEw++9Cll@so>GfmD%7bHzs$K=kq>xIVbZb3)MGtWsFQiC ze1|pk*Z7m3poWqXr%ymw3+}6uO7M@_U84PlQblydkXsn2G$V=23GfMPP4#n_EZErN(1q$-)}459peRHQka13Y#Toe{w(8j z3gO;Eg$qcU$#h_eb1mJ#>@s6?bc_bgvXBzx}ekyCc-d#bqpa1^6ErQp)yO?8o%`(um+Kx6#$JXuGI8!enG=CPk>eoH> zIfEihRsQ~%+I-4#ea;$lwSXscabw2Rb?x9l&Qrab7a-Tg+ zN?O|RsPV1<#58cOl=JbiJD0O_6W)A>xk29iPf?J1b|_IqMe*swLGWDL7224Tw0tM= zoYQ`WS&TR^bO<8OpIi-vM}^IO2=5-u4+)e|AFjjt zHOJQlZiH>@SD676o{?4x_K?2)nvg)<3IbDfa{eU0GdA1OJxY?D2C8ag_INhcIcc+M6J#_KEyA!LI)hz9(ZN)w^Q z#YI@6I0l75zyTsF4Gq>fKT?@^cv>JhQQ(k7z)mggJ#2=P{uM{Z@B%j8TcvB#T;{>f zWG99<=v;3}m@-nQ#z4X&YDDye=M;AQw%sbYg&0I+028_O9J1-(L;T)Oj1eOLmM}pP zcdU|RLz_-|w+pIkYYkDF#Kc7BQb)4%c4F^ya)^~Fvsehhig>L9hJnI7K}ytdADecq zePVt2&^$?f!bc*(9jbUeSZ)4z`%7LL~!)o{r&BsE?<5r*V;4oyWZr|n~fi1 z_p86a4$=T|Nk>hMSXS$_C;M>BZkLzUB%)rNrE(vl!bb3Ko-?*mH`n=v=FuGpMgRkaSt%yuxhPYj^@ZqSvw&-zrpyiGL!< zgTR;&PAViXzu;z#A*iu*{Fc|~va4j?u8(DXe7uM4_*3RhLM%8lwW448lkk0p);gGT z>|VFr%X zG~SlqNE^Gk+B|v6iE%_@g2S98EfMDxasNxj3|_WSa(|7w)eFk&9J4e1{_WvL43`5x zTV;;EzP^xdHA15}1ETsmJ3i>$X`k3>@0h`mVQ2GW59}e^zuUHUyNhn0mwOudj>(`b zcU#)-?(Xd7hx=xDFF&G|-S}fdsv1h|9}k|MpZ|R4cbZV*F!vl}){Zx#?P3%bFf1;D z!Q`O)Bp!2FD}J{NQm99{3-{nISOGx@a#`h!qv7OC*%td==0Lu!YQTkMu{v`?#Xu!) zk6YzIousrhB&Rj)d3H?&hl^$3r^(qJ zFO<5p(!mMs3sS}!d)O+AU$pr0@xByZOAii91<0(wy^~8g^ zI~}t_Acyc`lp%ED*zbI`f?_+tb?0W(8H7vUlY1cGXrLOhDQ7zYPY#n=jBEy-7i4wp z`C@0ksz0~YI&qkfevgl==lR?(+VA|!i^$~wRQCiG;CCR%0Psfz9ZYe+ab%N_fe_t85gL2!e%KZ(Oqu&@@uKd4TFD?S&U zf(u$1fY%ZER%|;F8frzlVyBb13$6y8ahOw)6m+sl-Wt{ojp%AFE-tpfWv({jL!5)n zxHk30+efbhw`$z4pYoZMs2_ja*&$9?SyM)(prFvMfK7CaTaV*#(aF0+VVEyb(eF;? zcI>KHM=57ZFTB-d(ysoOIZt_P4raOSUNZ6#bAiHf$Z3CEmf&!@j=BC($&M4M&P*_G zvb}7|vA5ns5m|2wUXY5|MyEV2xWu9jCZreH52`X!4Z`WL4d{5L+gbnsWF(1_va&{% zCoTxehu7r4wMe4okI#9Q3@2r{=+~b6J;iS=7|R)oR0etGr~9?tcQ(E7(~UmGMt8p6 zcs7aa(+vi_M$(PG_{{%&xw5wQUN>JeoAck+1;ik2@SzfCT~u8B2^52ZAIr(5R15Wt zkhLXLP1nt5N4e`njgym;-TZ3UySguC;ax(L4H~rmbe2Rmo&R}{2bIF#_Rx-Jrive4 zSx+K4rfHr`)_WQ#GPUaStKEx%-ixM!u3ZboZspJDnsN@P8dIj-~??>Du6$tMp49VwiuCA~IlpXO*|)P<>@ zD#vtU*wQy;h;f)7)P@Pp{uWMyE;Zg|WoJhzFD?QEduaStH0u$+`xQgY%GjM=7= zR&&T)mDUSioeRs$?d^8c-15#O6q%NcR#s8_ryGq30kVMfg_^CIjnp$xj&R}%b;_CJ1dLh&{13mrwd;Z- z&Zwxj@?qqBzA(qDZjS!ri;K>M&r^*QWfwD-YtQvOuQ9d;Q|WUwUs+49wEAHWIqYAd z!SnJVU0*Cy5E7E62Eb2h_3TzJF-1m0^VN!0gVPmKI}17^Ubinu$NlXJEiZp1*s~h1 z_w&6Z1N1wqPVmcECLkmJW2xN}`HTe`w~wLy=>k6UN;wyI)mi|3aqQLk1lUyZR~&UM z{3G2ikqP>)hXZ&Xqf8LuCr{?X`04Ya7hBr$+|l!u=N_#n2A#k-*h7y0t{OJm!`KBZ z-)!MX9?m}3gBemBz<2$n>h-o&n%qa3Kcr-(4Nu48QBliJq2qN46$S?a0FWdjV@Dxm zDaNQKINTo4_ONi9F9y)_Iv=w;?2Wgkex9tyRIoW-HUL@2uUhS_Yu2b!ehvjs6buXb z$FRcPDKAuLf$OD1`x%@lr<`80z5O&~W7jqsDEXY6zrTPmM}IOZv+D$j`=}`%r1upm zK%v&3u(7dS9L)F!N?NgFVKY(!-VmX|HjpA}Rl8SM>ii8DwPoS3HlI0dc}v+8>+;5U zNSIZpQC7af64w`i6J*8fy51Eu`BeTera?W)rQw$oSMbPoBS! zvYKtA4#%eMGMbE{?m9-==#5=D`T=n7{#+RboD{*`8Fh~<65tRc|M+;q$zKuC%DK6@ z2`Ysa#DXn}T&6WqIq#$n5;By8sBt4d+AO@ju~mpZ=oG zya*5*GC*vYuL0Suw3-XjV4)~e8eO8`!ikAy_+9ub%Z@roN(%3znz9o1<<-uXGAX~k zZ%UqS!^@4{xSy0vO4aOXlmO3}f~0`m@R@~ArTUnq{L>;Hwi>tPVqA)}taK~xyj=(z zRHx1nL&1dT33WP!tg|y45VnAt6AXv~=J{&Db$yd7B2|Vsf!mF$49Z3m(qU+@dZma|VF7nKPAu$`H-NLP z11ZO*Gp#zp#vkrgvFUn>R_$F~0pEIuiWpXFgJMh#JL~zzYKXJ4eyyUpyFWFUyunD8 zl9Hm$sTG+)wzjrj->kA(lm%K#tE-?O{H$q&#Ln04PP2q4~18_p657*b%w$%UrL{(Vh%IznY zTotM8=_v+>)>cn)8FQV#8G3pqa68;s^b>LZ7Ew9|fZTWvsE&pr^SwY_k&=;FdpJ8i zHC#%wOsFtEAyqA!Y3`5TG*bQi`K7S1@O)rV5oQ`CEv;Wu2)x#+#W(dVDpam+Yykc& zpaCpInaVHH(d2oBZOZIrf=zpOXFEyeG#{=ZCDnt+qUj6U+*12aQnMC5$HBzNn3y3J z61fC}1w#tWMhh^Y)0G8VP}_@nm&3+7{_8)#^%VP24~H?h40$?0A@aF7|Ma1WT$w(C zcCI5N9&ks$ffB8Ml1`(5N>Y(~p`$;VKEz|tev{rTj`?mt7+VxXQXx&L0#0d7F(A>A zdu9<+ST9M!6E(Ii;WG$?Jj12$0ug>qkwY)U>}jn4ttXsfV$Xyf#dvz zTL0lc=BR})IADAkMw8KriBj^G;R~(brHrbhKs9d0O%e7WPynuAOMNSH`^{-dYal$; zmUnODR|NPHUhqNbBL4w2mVYc4o#J)mkGO$u%&u^n_Pj{Cf6KQt>|2A*xQ zX}^;PxN)ROkIjlsA|kaJKEyA@b$91J;iPvIIdFa4&01pBO68?mD=w*!W6HNppc=5> zW-|hpD9}55?QgIgN;ml$&eVa|2|q(Ew*zP~h2yHmrq*#s((`(=*{NDfQ)c;SH$8q1(a%3=Lrs%HcA7Q5Xj3G&TJt&o+S`uckIwd0k{YAtE$mr$Xr zH)Qv{9WwFLb=D$$DnO5R_&g=rUI0{i{mv32`syek${S3A0X9`^O*j6@E&V&VW(#@9 z&~qL3Quib07MGn zlKD{9#%?qMBVhKfs>}B9N~Rz=3~5mF{BRFR@MHY^C~Gx0G&?J6Ri2SCH$gE| zoZBu#$Hu&0T1u)T6=;_nnc`ue16_8AiRTMu%RSM{_R~GX1w3jWp7p~+tO$sxl+kMN zgz68nwO5B>iGC-LR@UT~Je^R>W(CQ8>D{(lY7;7~7U)DpwM~HUNC44|>Tj+mulZe4 zRj&F8#rm%R@`4bJX9O%AIzSiZ*xSjejlzW!&@uvM&EPs3T3VyYF$IVB>k&`!U;1i% z*<^)?IP4OI2Ga9++%QAGGbpAs;f&^zU7HQo$8+R^7LCKyBRQLT&{KAp%QbxBPmvA|?!a8O;*io=KkVJX<04D)m3MuV6i+T;@ zWv=J$IM@oOJ!uUIirIEz_tHSh=GcSlq&kiFH;(`+BnPnHZsy-YmgTh#2Aqr)0Sgn} z=A;{S0?diWv>A|JX|W)0_0;({>xF_yS16Mj4x?&6rkdqUEg81c_FN7qEz%4rtF)eb6SUgVBZJ!+ z)?Og5lh&$YK55xrF!@JQzth*2nj>F1KF4Mv$$^9;-yxqM5XR=w>Z(8a#YbW_@2Kg4 z5E}c^fgbYEcTg4I9tt$lW-@r@WdXpfrNH{RdqBSHrQ7qeCm87T^m(eeqMm$SRF6SF z3!Xb7Hy}Cksh#*-P;d@};+S2bquDYBlWa}e z>*Kj&)rOt@H3{df+be<9F_yQi+9TOoY=FHxkU|cl6A}XR)6frmh#MP)*T-A0*$h>S z77=Ya;G%%5>yw)7w|8&)H3_hT1bl@jFw64Pih=_J^MRmk_ja#RhZo42gEz)g0soMi z;=8m&{54w0(tr-^>TeVW&?J~O;oNzzi=C;uErvH=V#ucd^z^h9q=P6CE`xc={ukU( z$e^p)Bm&@b9QmrCXJH%vqf1~d7?)n&4%RN!PsD$vVVBn`(z_Nq!Y13Pu`=|$qktpR zKUz8VrAl+b22+M42sr%o!-o`rGTHMz>bgKJjP(u-Oct6g=LTe^OXH-j>cj63RHtKE zXqmR+XK@D{=GVbhyw2OG(1COTL{r%$_7|qFtY^r};)QST#|VmfvhT z+ag}IF#~7BEpr7-7u5qHip1ES`?^=El=5hbm72eD*IgX$j^#buKYl+p*}d)~~qcdop9jc!Ms1#q-(i7hKHm(CY&g3V+j4$uVF zHPg?9I(=2ZmEqJ3$k=v}&AW3!0UO>yqF3ysQ6!143|>JGrimqhBF(YZT!rFNzO$A9 z*SPw3$8H%@PFkxO>nmY$c6so!4A5&Cf4Q}f2Iwe;a3#IQ+=T0R)UcVKe-Cd-A&~1H zrm@FB`h-`ro|}suDR^*GQw|50JrN<4|8z-zr58>n@V07L7&JInL)GF~epsbyRzv=G zBP3f+_Peh2=U%|DF^NI3@PAjr2tl!f@%jK zNiST-?2GbErE)exWWkA(3ox}umeYm>ILc3>MbdhY$XT3qb|0;ElB zQ4t%QT&jTbvIftE#LoyNifwF>G=i60nHsCE>_8dR-q_od0d*3qCl@ID#>c}WywuQh zR}h`_G9wvcjGpEk^67jsKr<@b#hpZqN|~;vN+MtpSPtboBj5tv1gJHG+voO9am-rbT%)6-pQcu%N}XL?6!XSYLAkKqkcJc{aqb|e zaNE4mgXc!xy_|G;4uerypx^-NUC>7H~Lb9+@3oH&eafObM1<1s>WXa<{R9d$s_?i2j@H; zf!Eakgp_vg>qPFc6gU9%s~$Uy5NTKL_cVJ78Jvc-yc86xDokYnIuB4zO?D7~N|n+q1M!|B{{msfI3Hn~?g!nXDjds?o5Pc1bz9k;n{tDz%IRF! z9M3ANmaw)DJsgclkW5I3wx@!Oy3VFj_%Vu>ZHTr>5OizABS@vKerxAKWOUPHJYAN( zXG6*P0LjE-)lR^iY?AP-IU5vk20g04>Q-`VYLN!A^Oq|5s-3B-jn_}XC5Si}&eLLe_qXKEqdgK7=+DQF9)oo5D zP)=*u<1RY?b3D~af37sGUiIwWZKnEONOZo1+2;IsNB>Od8$-A1wlcp0B}c6 zRk~OEKgI7|v+4+03I=T-;Oo~WXPrdf<|^Nb((r&5kYu>ZAI(G|TZ)y94Kdw7b^dgC zHYdze{nHOvD1b%~Hc{r(p#;VE*tR)_9Wc<8V$jS&bukz}dmre2U?Mti22QCn^o6u8 zDjW|ullVO&q~l@H;*7|R>53)sd84Z zgSQNd8ZVxt9fDOKFC#kG#2OI&Js67_|NgNNwo797ex}r<*iu6wku$qT#@}GLNbm5GFgbAcPDxSZXM;(37Orz93Pt-?vdbq+{@t}7rZ3VxZLo(UeX2g;Nzrny9 z7#OJh<$+ez19}(I(4hj9)XX%Bcc2FYFs6)d;&ZU(KG4AGT+c1z zwFgxhaO>~Ko(f~2C+eI>EfK-tvdjFOjQ<-kP}*thm&WYfGH@Z+2jIGrY>FT*94LYu z{&Q+`t{IJvaaBjX;LaNXaGi?G=LDb++y=r1|Bi=HK>YE! zKr5%`_$OlZ?cuI-hOe)lGn*?0xUaDlK;gAo1ka%a14&q;j0pfyQ9+{h{&AB9yGlk{ zdNUbu$?oqq8mnI8J6Bi!`}-=z(sj%6LI)90YchUF|8GXTYaavpP*_pZbzGZ7IQ)JR zXf~WQ{Xqg}_2o#&N%`kH7g3s{yS)GkKA=X5Jh&%;9IKpVV*tCe&it4wfCd^Dps@w+ zcnJN|g20G@f!#}KAiDqc15eTaUw;G(IQca8t)QT;+lxunKPqNxxW<#%O7$7Px`qY> z{WkbRDJY38bqE&=OjsvKC7C`%P1Laus|OO(-93V1g{(YGbH0?~miZwbm$Iemh<~(O zP~6;^A!(G3_E>99S*LpScc>(MiKep?8&C(61)Bn{2>l2>g}5kHrnwj@)@b$=t)zJ~OX_#h#Sy)`=FLT~N{j zTVUJ_7{65)D#b9<-w=)OKq5<%T)#&G1C0&_&%c))*S+7{#Nz*gI}@y89d6y&-jy?MWH6&5Pls|yj!{{<}lY?CDr*$p- z9sZIkeF|f1lH!w!tRN&Dz2?#Ln)Jkb&!^|$?;SXiG-v@Ot|`Mj$}xSBjv&=w^u4Mj&Yyp6~;CLt|J8=m!q zv2M`3oz&Hq+~N@yoiWT+^xgA+Rcr@lNUPxuI7wDGwH9Vp$CdHn{GU%p?7Xxzl3lmf zi!f7_jA|}A>@|Y)>Hl;ziu(myL`^r3W)Nz_OnR1~u*4z)#~denGXi^ASh(4R{Rsy!K$Pn zOKl-ZKB-Sje9RtSI6Bzb9PcO*z7g6oSR!#V!|0-h30g>NTdm#Ii-qH!IGs|MUXHRm zqcg%frzqwIiGoik0y1)N`K?K2q$jNuEZQmW85}3Sgz9;GwRpVHS!!&<^)uTCkpE+s}j zzoD6Vfv(&CoY6mXJLBxb4ZLL4B3LiCA+~N{~JRyVPhgHMoZE?^k%3i3$ z?HO+uO=-@?nsr7(kzr9X&d}xwe1XJm&s;Yr_oVm-G~On*U{xQ;b!rvGh}NWXF6tCFdbyIh zI=W2lRSXN|@Kf3(g9tL2O0x(p4YlfL7YX-~cG1I)U(|0l4TBAI_ix@JA+fD~m__}p zeznjYNm==jAYIvly@&SapzBCU9>@5g!_VGx?frL_xEEqq3)gpw%Ly8dk7_~i?0v85iiBah8%*@= z@Ps~@n>}GXI4p3ElUR?^QI+@ow?E8FW|?tp&vTKGB7TZ@m57E}h{v5!sTPE_l^lgq zCVS|jRxOeQdx(LkJ0cg2?^In~wY)mD9s^a#sW;%aItjOQ^R@NlXx#~P& z-Ej+1qt&J0h1ctEX-Sf|aucU--1v?31+>yuOF!{zVK8&JST36z5RtvGZIYavhqJsj~$+ zQaOy-fTckFk5Ta)6hZhE3j&j#ZXZ|3?3ac3C}XJ^T(t7dB^=(U=gKHk)P1AaxP12L zkchYH__9os>n{2vdkSy2IC*KXhN}rpNc;`bai!z35xThU;VKi4P~B z{;JFukNc1x`!!N*+UQ9Xt)m6%S`{t(`M^1n$|dzifX=IZ!6?cj&i3$Ji8u`^Rn^o} zYS)l3>5k`+q{1!fO66gBjTUB*y=5@RIgL$;x{0yWDM)JyUz^r9b6Q< zWMm>bRY#WO6N}_d7`JEeH%(k%^(`h;!uTg*f*s6(ivcCqOtL8A^ z(>qfuLmN@_pA~oBq6OY9NOFY|`|aY!L1ZMYSC(kZ>i9A~r@~q8O(Fg|^f>k%5!1Hk zSIRpmMEGiiS}9?MS#}F0poj!#LAQ+|D6b)X<%e(WOZ@dCA|AVc(nL~itdBD#sMxAy zqwa|=rN{X+&_rtkA^Y{bP=esLeub1jrTMo58nf9}gqc$%3PH(yTyH^_d$%D5I$eb! z^=hSTCt+YJ{$Qj&Q=il9?~Rc6FCVuU{T{JpR-{gjGI0;YH(#^a7ohGwRsL)9Zp%Y{ zI)usjmlSffov%MjE86yFV^>wg^x zIL1t@cbavazt0@JLv^!7HjsN68Qtkn_O76Se?#Ig0ZD9di^6Tj zClQ8}zD17y&ua<<=4-ki1n}HMi8NHhP^rdW_zDTfE!|be4R>=+uUg_ETF3-&eSU-5 zp*pjzowhd_h6>fxhOjk|gj8f;GUi;27Pk?-NRy&vg?wMiruf-Kat`QYH|HNWtY>b> zW32xV;-y$$g&CvwHuCh2^@U@qsbO^?oYI+p5=uK)`)t@nkw|0z%>T=_4MOH5 zZE;qr$ghPNa`kpQt(RrwXH+tujFd&?BZ?`}jC$8=b^T}7WLb$qz5q_JY5Bt;Alev3#K`i048lUg(plnkkQQ( z(Ln4%gJm^-GuXTAkb=1rQlDSok5-oW8|-Nb^q}0I$t)P_@V}FIs(bB1uiP2Ueb1!Q z&J$-_X~Ia2u`at6OKW1hK`?ynie8vv^p(nVi4T>}Z``xuE0s&T*P_(cEN3Yzndy=B zj|Ov*j%(wEyEiPBGSbdYZDsTQBRo7&>Ce{v(L#&nMQ;~93OoyZ4F&|vS4M69C)mp*R@rVN`rF zdu{&?qul8rhc~^Tj^cN=r4-tcm2+Iw6MD9+`x2bePWg@H-JQtKrqjVWBnEaYMS@zL zk%CDA0y|{Ozv#vff8OLHBc0N#6d$Yzjs_$VtDd!D(|xoy>6i-X4#pe3KGVs%Uahe{ z5jMmmzz$qT$Z@iRKEgTLhls=%+}_*a=bQS^kelmEQkd!A^>O&CSkFkTs?$?rXiMUH z9q{>2+!x1=qK>m#Q16Xm^D7PPmel%(QY;5_)pb>Cf@ewVK%E zSGVejRL?VU`#(>2WuSy~_p+^QClA>kH3@V?dQOt=8hMzeXk7ZDiPFVIUD;wZpu8Y5 zqcr6&VrHafs`@%OWn{BKiimEep^g$|W)Ca&H^|r9GcJx#evwiidDA7`{X!ttCqL(X zUaA38nw2kGm-EKV4th^!O1nx(nqLg~0*Cud+Ghr zM4#rwOgDvkUncSON#2oqc;L6GRvpq0pHjVF6p+;SFhOh6Ts)*Hu_k_hqw(c;#CfVM zbODzRD^Xp1wTt^N`KM**>WR?2kbmg^o$+_Kj3R|wi!YO%TH9YhniqBb|5oCv-@i$C zW7Z}-tEuIZUHaAQ&nhYJm)hilF#$fVM?p@GQRy<19$q!U6I^W;5!~!q5wD8xH;CHu zs`jaE3t|kom#hrt+xDil1;PsE5%`}UtS*Xp)4b2~wEwmnFOb{N^tAwnvwBqE6EQ2v z*_!RnUp3B2BYW0D)=U}HXWY=_@i?+;WKsnfXu8+;-qQ+dbNX(Vf{Ds}|Ksz*wCA~w z{uRsxJ{)7))9P)-6GaZ3&IVXgPT^S5hYV%GL?%P|Ie~)ea;5ZQ|9(2=`TT57DK(-v z5z4mubJi&8u62G1-jPme3gZqoj=ZD17Jv5PjhV0FDU&Mp_EkbY1k*iC@o5f@Bv;l| zkY;ss1}wJ%h+PV*H2uZvBmsrF8R2oEe=;$abO%VB%nvh3=8 zGR-=P9eR9M@0D1PN^j7S!3hoXK0vuad(~i%+K6M6#fbZ?S|^IMG|7vjyJ0Dhb|wb$ z9}hqAAi1FA(7ZiZ`2)=ZZvRfHa%B&r-#S0x&^e?7jY zs*gO-kArSsHg-y0>!tNd>cs1NdW4R&_LH^A#SSqVW-;n9dNVbk?@(imgXG6hLmRpl zvNc4(pq~|9Iy^x=A_U$&md*Hy3HPtY%*8kdUt5SaRm87K?^{&C)J`?{rr<+oL zsxm4)W;8w{HM`rOJO6!2@}9P^JcyCUnRvqQ&LW&59ZLP08+Pz|(*FI3q{vg+CjOsK ze1k?dh-FPPJ2ZKm(&hA->#X=iCJXr(e-$}?%zD7rd|5vp90zbPitYP zX*~YR@#e=@wC3O$Ma@OwmZMj{LQykW@`}F6boKDhh*Q1}8ad2^47)o==G#dVvB>_o zH1eO^guhlkr@I&9zcQ2+L;BvvlCq>~ZA4*Pz%&d-fN&R2%vlomMg~)3&NC^) zP9xM$&-+(=IPSJVNJy4ewG#HbAC}Sg9FFzhj?) z#X5c2^)96e!QAr$+#y~)IHNel*k1E`@7B3WU1UlA^YMgrA>H3fxM$e#XRgcB4!ijk z#D(MaYj;;n3hJ~E%&VPni}ya9eiZ-AhjH1Nwx-!f@n)2ahEU3>+^Z1@F@PkOzdwgT zUww7jb2t5l>9u=NI0jOyg5(BU&X}QAhR6VgzoGv0UQ;wusVm>N}M&dFOpTO)tcf2hFyvoQURz%jgL=L~+DI?We^s-<_xr34N25z}|b z=zVO=>zW;UH`L3d-e*FL-Ha=)1_)cq&FaF*CHSNsToFmv7=p?kz6CJI>d`(G1re`T z_#2gTcVW3CtqFyV6qghVeMy>#nuvSk;X(SRjDy~mZpyFJ9^xKT@)I0}#eSG5$0c_Q zpImyg-!ysb!taKhp~pgXJQT0LY(uRQaE*tIE<-w}aHs6m;eRwy;{7JU(%O+TA8T7c zf--%{av8>lpfYiG{mM+Z&U3ku4d3<;nxD(DxfO$KLUPS5Yav(RSGIbv@aj>8f2`_7 z;3KlGRg3#WgvzGk=mWOMitP=L_CvPiyt=0~@+%Zv7GO%s{v2Wy8s&yJROxb5L09C4 zO9e(9->!`}@P-MNBy5qM45m|mP!UR3h}l#MNR+K3q9uH$b4G23U^Z8^la{KzRsOEJ*P~K@@4`D7}CdPg(WXLX+F|-8@QnNC|((fPV{QwW)S;#ZrV^z%@s%bOvhuQ%j|cB+&_;azUo@h z$R?2NpmY+3uW7Cw^}ZiXrG`7P2=OxtSN0UEMvSsQ38Y*1Xt$k+PvgR7UTy28LGmV zM_%lb=ROAFH$PAk`kQpEu)PcM@Rr&bD0IDT-AnM7aQ&Z|&@5_n42(Bb2e|s%Tjd!I ztkNgCG0StjAEih`_{BAPOAjvAYtpbyj?R;*yf%3s{G?LrwB1&n)rIHT~18!KFhQzAG(;0*K8ePkf7v z43$t=b4HK5)f3MNNqrZMgy!^6KTJ9iiPa8?>Jq`%O_dP_bGw@*t0#6iU8^5HyrV>$ zAIJ&$_n&c;>-OfNS9sg;QJnwGK%|NZ!Tzo^eYfkBXeV9ijj-OQj+o%D!Z=-;HHkNb zmmh{A2jv_JG+;DeB03m1A9i*&H4!ON4AqSd(QHx_#-8=3ee7l-Ru()U0|p|7)&yy3!VgyS`WC)7jtoi;c@>3*tH{}ulGsUi)2}gcRF8Ic z^f3=|9twTibzfbINX#6aGc z!^A*^B|<=-K6gfcecaNc-26)1^})x=YO0;|Dn~EnYw%9!_bUgH=tELKyhaFElb$zy zTGu>@e;s}GJEHl`5ZY_lkEkfs`ugDJ#6Inb+fH$DlB}Iq-#ZPFIz>v*w{zAIGE+Pp zXuY?Igv+=B=5oEx@zgd5j(b;gZFcayPZXGul8OYlZWj|5m>ZGYDjJN674E8Z zFehf5E%te%7Odbt-TEkV;^CFpy$=V%TDtm^zhk!Eii3@{(QoY0(`ORf4#t~YgY)g4 zIh&k(tOn4krvfQS9+Ol0hIfFht48I)?s2D>QRw%3F_Nj5I;EtnWCm;b3;jB!kp=U) z>xkyBU$;y@73M+QN@%D~?Y`?H8jAD_`PIhuqDB>#9Y!O(OU*v~sY|v-tC0iMH{cnQzN)Pfw8EN%t%?Bfw zMhd$W>y{vi~pKT0zm+dWN;?o;XD3SscDXEasvpb-(2w^zku zSdMLCrevz}o!~l~W-m(F^r4r`x!w8n`Nyx^xJZt$KIdA6#%=m4>4MvDJzyWSS4fUR%9X>k?OqXxj z@Z_&7T2Dl5(56=sN0QY3b`#9rMzDaH8gQFX=gvocphKE%FV5%2W@B`h^J#WLcI*&Z zX}KtAFVB}9QDidT?^iCU)mp4!{y-ANw8h}@>2VOIa+aI2g)WRn4rR86=s7MCg>Y9L z%8OiU;r3<|$wsHZ8HI}G>Zks`W-gV!gDPjgt%XJT7ZmDEsJZLv5+{m7fTxZR^&54( z->}%@l?SiXy5yl=ZuD{}gSJe&mxo{f+V-Z;N|siommQHAUhE_H-{DkQp0x-?3cm%X zX4t2ygv*lSeqfy#-A1@1R3UowEx=TC26*3&y-4bSdG5@({G6HpdPL^=GInp%LU)Hu zWaF^h{s;rsugb`Lh+g90KHr&?`}bADykzzA6x|<2M#LI5v;}F2UH&c`uP6lHf<;Lv zBXY|vi1Hk+sjlnrw${nX%s4J1d4rtv`(}jk)Rfg&G9k`X=94a#iQJi%w(RAXO-X@l zOzz>oP7lq+HvzqjsfvRdRu%?wR1IVp`*&Sd)q;|0a|FF=6kGgJoRBxGIOA%*#Fg8g z+k*35^BaA{JN<{q;QWA2@z!cvnoX`Q*=%;9zw$uqcaS(^YS~vw17ZoiLc{6*7ak}x z+0yxP@JL7M!2dO?=lsyEH4m4Yt-xAiV>p$8W*W;+m$%=!2dr&57KRe3555bJzZqM) zL#%?-o-{r7_%0rAM>nBxnu+^yFbF4Aqxm3jp(5S=h9Ol^{$MqBTv#|_w983Jr<+jk z@Z+v2D26}nDji7aQx9|Ujp)mo8e)DSZguxkNbI#xfA^}33W=xm_?bMNsYoenNtI3{ z5Suik$iE~YQF}>hzA3RI-KKECY6Ofcro01d5qw=vN(E-xGY#u6G|#MdWm>!5?+h3^ zIs8A`x%O|i&p5o*y4AFG8!f7sOpB>|)m=^8$H8O-V^muem!vc`myBY|Bqc#zf}Ewq z5LvbAT9?%&qli|eLBhmc=CZg%M3Q~guiMW4fS&W7_kGX%ob#UZd@kob&-RpMeoI<3FhMtZL|01A%^{wIRzqYx|DbBfduaEKf5eCg@^lY8iI{9(OvS zB3p^>6~W{QzzH3x8SugvXaAo|IJL>Q(9_3d=|4W^Z|VX`X+?PB`=qA4Y3$^{KeG-T zj-!PCkzo3=4+}~bS_)q@7mG%r#Q}Znla|Z?aJVofTn*Z?2e-d5W9=xwT!<1)b+CSF zkG^Acy4aTMrl3J?DPGmty=lo$P1S3T9wga%sz@!E4%P^E-NU_Y-B!wuQ^en$1+E@~ zLF~UK+c;+RwGm&kk3B~40%qEX%aulf5oCD6hK^pe+C}@8{O-(;YF*7PMN~@5Qs)Gc zN4QBE5tL`&is_B!o~_~ai9V6QWayM2H$@f8BuZM0ePRAqixTn;tm?B@u4~zajj@{1 zu`qQ4(I6sVCr4Pqd*WO%@!El_^H{GaE^?=tY)o0KOEMFYGYROmm3b5jX*UyUM05}r zX=wcgRjJxiZDsI;Juq>i)||j;2d{eB@a9ADtc`PNP+h$UXKdne{z&MZt?C&lcxp`P^@!e*Mcs15qtx{^xeLPa(38Zo?3Ixwy|*H3Ag26 zEbZ$fJ&YYn)*Qm_ykxN)HBEGK2T%}aj~+cTX3i=m1YQamZy?9M9|%N19NRm=x0qr) zcBKueSori*fdSt)C2ry^Kn>Y7>hm*_Q;$a`w9 zpWigvnkBRJJ@U z1#Dp8af`Wo*HU8tOB&qXFOBiLDM9rn+X=G3tEY$=X4&(&A2)tpL?5>ap7zp2aS}MF zF0_w0MA`)jF%}YJepmH#ePLbiiH;vHs78*87nD02Ud5M*tS&K|c=Fl)c97@oaofH_Sa$QX z!f`2&iOmkH%Tl;@GGcoaa-B|{gGN{AlCJYDNrGVWg_W#PmV*etf-$QiDU_;}@B!h@ zec0NX<#5gOe7=N&d64bzsX|L+Y~-49t3G5Lgy|BbGXu9R>U#%2R>8Y`+WAB21?A27 zB;p-+$^A|{>|pg9-e&ssV|}s= zImxVJtp5`5L!+XUhO8*{4qM09!!CW7^QxH+Z-;W=Bj=;4YvxX1gfjO#oARhJffj&S2j5ij1 z6DF|8$=i<87C*OfzfrIyrUfVNt~pPue-HLJfAv)tHb-3_I8D81F85A)p!SrfX70V} zGX}9$5IE2Z|0_}Fl`}*PZ}U%t^H1jM56vgQP6=k`$nI2IEx#2->$Y*ju4>Vur6NdvHq^q6zb^%`??|1|a zp3pjiA~W-jAQMN|BS;0HcQ~uWyxZoTZQjMr|4)X!F1g5H2;-Jm4yfk(o*UT9iRu`Z F@i*ZEpSJ)2 diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index 51998d019c66c0cbc17f3c3370adc36e07d6181b..9d52fb30b7d65235550c501ec11f2306ed89668d 100644 GIT binary patch literal 15123 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 YWlej&7OBP1@0|}6)fEcwKYH;$09^9LfdBvi literal 11228 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 E0F#*TO#lD@ literal 16994 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 diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index 7b96f3ec43c7ed6cde24842fb49ffd41f02133be..c43169bfb71014d53fe5cbdc7a75e82fc7f32a13 100644 GIT binary patch literal 1994 zcmeAS@N?(olHy`uVBq!ia0y~yU^&FVz^KE)1{6_bl>f@Wz&_E_#WAFU@$JDz#sdlr zObXBc+i!p8@xVG9sD$A`AuovH$pDcF4$L5mSqel-TmX>=8bBs7u$h2Jg9VI0vVn0_ z%V>y=CYaIuGFn^!i^I{<5*P@hl?5;mh^;H@SWDPK_W9mV0P11zboFyt=akR{0B(}e AN&o-= literal 7054 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 W-lz9Dga5PXQMT{eb_F{4^}hjykH1|2 diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index a7088de3849a54e103677f10d3d4224921e8feac..4938d13fcb41d3de1cfcebce7274e1b178a8497f 100644 GIT binary patch literal 15647 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 Z{Z=S=e{N3V7WBkYSwZb_-lJ!4{x4>};{pHx literal 11827 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 XKy)i_PxajewpesD4K>QuZruM5$4YVU diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 8f93ba81ad2adc2d9bfcd4645303607fd962a233..b3703ecc7a330b86301d0ff910e00bb32bb90c3f 100644 GIT binary patch literal 13748 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%0D) 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 literal 10314 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 bZTAH~-nel&0XzVK!A_nCJ>K-q=^y_K?U1dR diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png index e0cffd065fc4ad31a2a7f09d7f58de66722a83c2..c43169bfb71014d53fe5cbdc7a75e82fc7f32a13 100644 GIT binary patch literal 1994 zcmeAS@N?(olHy`uVBq!ia0y~yU^&FVz^KE)1{6_bl>f@Wz&_E_#WAFU@$JDz#sdlr zObXBc+i!p8@xVG9sD$A`AuovH$pDcF4$L5mSqel-TmX>=8bBs7u$h2Jg9VI0vVn0_ z%V>y=CYaIuGFn^!i^I{<5*P@hl?5;mh^;H@SWDPK_W9mV0P11zboFyt=akR{0B(}e AN&o-= literal 1806 zcmeAS@N?(olHy`uVBq!ia0y~yVCiCDVASDY1B$H4iCDcuu~YQf??lv5P84^L>lmdNFQbpc^O2E3Xg`#Xo48c4WmT?s5Bfc9tTm~ aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 98890b9687ac95a52ecf4a4299df070eb5e86fa6..f8de00f81926d78d1038cb872e40582625cc9b9f 100644 GIT binary patch literal 9238 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 mFo1ISpN??m3@2hhkpk)MYIF} diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 479280f598aef7d02121f8874c21c223c9f642dc..f862a9cd46c662117b1b5fb5d613b0f737601d8d 100644 GIT binary patch literal 19125 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 literal 15100 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 aHE&iz8^0!7DlG?zEGmi`4~yiVy!s!XBR4+) diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 84203617ff853..550b3b5df12be 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index ae72bcfa6d5ec..cf488ac7f3ffa 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json index fa5892190e5ba..0a47cdb8ff74a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json @@ -1 +1 @@ -"[metricVis] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 8568215fd9e1a..8c272901c4e84 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index d11e1dfb925f1..abc0d3a446987 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index b160e05935f17..1809df5e709f0 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 9c642e5e266d0..ec32b07ed9f2e 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index 4241d6f208bfd..09602eca4abf2 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 84203617ff853..550b3b5df12be 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 153eea71dd8d1..071172c698ad7 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index d5f01afa468ab..ad38bb28b3329 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json index 46b52a7b3eaae..0c50947beca97 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json @@ -1 +1 @@ -"[tagcloud] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 72b5e957c19a5..997285adfe5f4 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index 7cbe7cc79882f..10e23d860637c 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 84203617ff853..550b3b5df12be 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index ae72bcfa6d5ec..cf488ac7f3ffa 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_invalid_data.json b/test/interpreter_functional/snapshots/session/metric_invalid_data.json index fa5892190e5ba..0a47cdb8ff74a 100644 --- a/test/interpreter_functional/snapshots/session/metric_invalid_data.json +++ b/test/interpreter_functional/snapshots/session/metric_invalid_data.json @@ -1 +1 @@ -"[metricVis] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 8568215fd9e1a..8c272901c4e84 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index d11e1dfb925f1..abc0d3a446987 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index b160e05935f17..1809df5e709f0 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 9c642e5e266d0..ec32b07ed9f2e 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index 4241d6f208bfd..09602eca4abf2 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 84203617ff853..550b3b5df12be 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 2760875119197..59de1f285799b 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"metric_vis","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 153eea71dd8d1..071172c698ad7 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index d5f01afa468ab..ad38bb28b3329 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json index 46b52a7b3eaae..0c50947beca97 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json @@ -1 +1 @@ -"[tagcloud] > [visdimension] > Can not cast 'null' to any of 'kibana_datatable'" \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 72b5e957c19a5..997285adfe5f4 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index 7cbe7cc79882f..10e23d860637c 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file +{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts index bbf45b003c330..e5130ac95b7f8 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/helpers.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/helpers.ts @@ -176,10 +176,16 @@ export function expectExpressionProvider({ log.debug('starting to render'); const result = await browser.executeAsync( (_context: ExpressionResult, done: (renderResult: any) => void) => - window.renderPipelineResponse(_context).then((renderResult: any) => { - done(renderResult); - return renderResult; - }), + window + .renderPipelineResponse(_context) + .then((renderResult: any) => { + done(renderResult); + return renderResult; + }) + .catch((e) => { + done(e); + return e; + }), pipelineResponse ); log.debug('response of rendering: ', result); diff --git a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx index bd343b15758bf..cd8a0e91510a3 100644 --- a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx +++ b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx @@ -9,11 +9,9 @@ import PropTypes from 'prop-types'; import { EuiIcon, EuiPagination } from '@elastic/eui'; import moment from 'moment'; import { Paginate } from '../paginate'; -import { Datatable as DatatableType, DatatableColumn } from '../../../types'; +import { Datatable as DatatableType, DatatableColumn, DatatableColumnType } from '../../../types'; -type IconType = 'string' | 'number' | 'date' | 'boolean' | 'null'; - -const getIcon = (type: IconType) => { +const getIcon = (type: DatatableColumnType | null) => { if (type === null) { return; } diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts index e9b580f81e668..60407b78ab5e3 100644 --- a/x-pack/plugins/canvas/types/state.ts +++ b/x-pack/plugins/canvas/types/state.ts @@ -10,7 +10,6 @@ import { ExpressionImage, ExpressionFunction, KibanaContext, - KibanaDatatable, PointSeries, Render, Style, @@ -49,7 +48,6 @@ type ExpressionType = | ExpressionValueFilter | ExpressionImage | KibanaContext - | KibanaDatatable | PointSeries | Style | Range; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index 64af67aefa4be..79d380991f5fd 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -6,6 +6,7 @@ import { UrlDrilldown, ActionContext, Config } from './url_drilldown'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables'; +import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common'; const mockDataPoints = [ { @@ -15,12 +16,17 @@ const mockDataPoints = [ name: 'test', id: '1-1', meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field: 'bytes', - interval: 30, - otherBucket: true, + type: 'number' as DatatableColumnType, + field: 'bytes', + index: 'logstash-*', + sourceParams: { + indexPatternId: 'logstash-*', + type: 'histogram', + params: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, }, }, }, diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts index bb1baf5b96428..6989819da2b0b 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts @@ -9,6 +9,7 @@ import { getMockEventScope, ValueClickTriggerEventScope, } from './url_drilldown_scope'; +import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common'; const createPoint = ({ field, @@ -23,10 +24,12 @@ const createPoint = ({ name: field, id: '1-1', meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field, + type: 'date' as DatatableColumnType, + field, + source: 'esaggs', + sourceParams: { + type: 'histogram', + indexPatternId: 'logstash-*', interval: 30, otherBucket: true, }, diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts index 15a9a3ba77d88..0f66cb144c967 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts @@ -131,7 +131,7 @@ function getEventScopeFromRangeSelectTriggerContext( const { table, column: columnIndex, range } = eventScopeInput.data; const column = table.columns[columnIndex]; return cleanEmptyKeys({ - key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string, + key: toPrimitiveOrUndefined(column?.meta.field) as string, from: toPrimitiveOrUndefined(range[0]) as string | number | undefined, to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined, }); @@ -145,7 +145,7 @@ function getEventScopeFromValueClickTriggerContext( const column = table.columns[columnIndex]; return { value: toPrimitiveOrUndefined(value) as Primitive, - key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined, + key: column?.meta?.field, }; }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index eb00cf93ccd34..c95f6085b4791 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -13,20 +13,50 @@ import { DatatableProps } from './expression'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { IFieldFormat } from '../../../../../src/plugins/data/public'; import { IAggType } from 'src/plugins/data/public'; -const onClickValue = jest.fn(); import { EmptyPlaceholder } from '../shared_components'; import { LensIconChartDatatable } from '../assets/chart_datatable'; function sampleArgs() { + const indexPatternId = 'indexPatternId'; const data: LensMultiTable = { type: 'lens_multitable', tables: { l1: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a', meta: { type: 'terms' } }, - { id: 'b', name: 'b', meta: { type: 'date_histogram', aggConfigParams: { field: 'b' } } }, - { id: 'c', name: 'c', meta: { type: 'count' } }, + { + id: 'a', + name: 'a', + meta: { + type: 'string', + source: 'esaggs', + field: 'a', + sourceParams: { type: 'terms', indexPatternId }, + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'date', + field: 'b', + source: 'esaggs', + sourceParams: { + type: 'date_histogram', + indexPatternId, + }, + }, + }, + { + id: 'c', + name: 'c', + meta: { + type: 'number', + source: 'esaggs', + field: 'c', + sourceParams: { indexPatternId, type: 'count' }, + }, + }, ], rows: [{ a: 'shoes', b: 1588024800000, c: 3 }], }, @@ -45,6 +75,11 @@ function sampleArgs() { } describe('datatable_expression', () => { + let onClickValue: jest.Mock; + beforeEach(() => { + onClickValue = jest.fn(); + }); + describe('datatable renders', () => { test('it renders with the specified data and args', () => { const { data, args } = sampleArgs(); @@ -106,7 +141,7 @@ describe('datatable_expression', () => { }, ], negate: true, - timeFieldName: undefined, + timeFieldName: 'a', }); }); @@ -150,10 +185,27 @@ describe('datatable_expression', () => { type: 'lens_multitable', tables: { l1: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a', meta: { type: 'date_range', aggConfigParams: { field: 'a' } } }, - { id: 'b', name: 'b', meta: { type: 'count' } }, + { + id: 'a', + name: 'a', + meta: { + type: 'date', + source: 'esaggs', + field: 'a', + sourceParams: { type: 'date_range', indexPatternId: 'a' }, + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'number', + source: 'esaggs', + sourceParams: { type: 'count', indexPatternId: 'a' }, + }, + }, ], rows: [{ a: 1588024800000, b: 3 }], }, diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index af1773b413599..6502e07697816 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -166,15 +166,15 @@ export function DatatableComponent(props: DatatableRenderProps) { const formatters: Record> = {}; firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.formatHint); + formatters[column.id] = props.formatFactory(column.meta?.params); }); const { onClickValue } = props; const handleFilterClick = useMemo( () => (field: string, value: unknown, colIndex: number, negate: boolean = false) => { const col = firstTable.columns[colIndex]; - const isDate = col.meta?.type === 'date_histogram' || col.meta?.type === 'date_range'; - const timeFieldName = negate && isDate ? undefined : col?.meta?.aggConfigParams?.field; + const isDate = col.meta?.type === 'date'; + const timeFieldName = negate && isDate ? undefined : col?.meta?.field; const rowIndex = firstTable.rows.findIndex((row) => row[field] === value); const data: LensFilterEvent['data'] = { @@ -196,7 +196,10 @@ export function DatatableComponent(props: DatatableRenderProps) { const bucketColumns = firstTable.columns .filter((col) => { - return col?.meta?.type && props.getType(col.meta.type)?.type === 'buckets'; + return ( + col?.meta?.sourceParams?.type && + props.getType(col.meta.sourceParams.type as string)?.type === 'buckets' + ); }) .map((col) => col.id); @@ -230,7 +233,7 @@ export function DatatableComponent(props: DatatableRenderProps) { name: (col && col.name) || '', render: (value: unknown) => { const formattedValue = formatters[field]?.convert(value); - const fieldName = col?.meta?.aggConfigParams?.field; + const fieldName = col?.meta?.field; if (filterable) { return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/format_column.ts b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts index b95139a00ec57..2da6e7195a5e1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/format_column.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/format_column.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunctionDefinition, KibanaDatatable } from 'src/plugins/expressions/public'; +import { ExpressionFunctionDefinition, Datatable } from 'src/plugins/expressions/public'; interface FormatColumn { format: string; @@ -41,12 +41,12 @@ const supportedFormats: Record = { name: 'lens_format_column', - type: 'kibana_datatable', + type: 'datatable', help: '', args: { format: { @@ -64,7 +64,7 @@ export const formatColumn: ExpressionFunctionDefinition< help: '', }, }, - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], fn(input, { format, columnId, decimals }: FormatColumn) { return { ...input, @@ -73,15 +73,23 @@ export const formatColumn: ExpressionFunctionDefinition< if (supportedFormats[format]) { return { ...col, - formatHint: { - id: format, - params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, + meta: { + ...col.meta, + params: { + id: format, + params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, + }, }, }; } else { return { ...col, - formatHint: { id: format, params: {} }, + meta: { + ...col.meta, + params: { + id: format, + }, + }, }; } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts index b3da722de5f34..5afabb9a52367 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.test.ts @@ -6,15 +6,15 @@ import moment from 'moment'; import { mergeTables } from './merge_tables'; -import { KibanaDatatable } from 'src/plugins/expressions'; +import { Datatable } from 'src/plugins/expressions'; describe('lens_merge_tables', () => { it('should produce a row with the nested table as defined', () => { - const sampleTable1: KibanaDatatable = { - type: 'kibana_datatable', + const sampleTable1: Datatable = { + type: 'datatable', columns: [ - { id: 'bucket', name: 'A' }, - { id: 'count', name: 'Count' }, + { id: 'bucket', name: 'A', meta: { type: 'string' } }, + { id: 'count', name: 'Count', meta: { type: 'number' } }, ], rows: [ { bucket: 'a', count: 5 }, @@ -22,11 +22,11 @@ describe('lens_merge_tables', () => { ], }; - const sampleTable2: KibanaDatatable = { - type: 'kibana_datatable', + const sampleTable2: Datatable = { + type: 'datatable', columns: [ - { id: 'bucket', name: 'C' }, - { id: 'avg', name: 'Average' }, + { id: 'bucket', name: 'C', meta: { type: 'string' } }, + { id: 'avg', name: 'Average', meta: { type: 'number' } }, ], rows: [ { bucket: 'a', avg: 2.5 }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts index 7c10ee4a57fad..e4f7b07084ea9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; import { + Datatable, ExpressionFunctionDefinition, ExpressionValueSearchContext, - KibanaDatatable, } from 'src/plugins/expressions/public'; import { search } from '../../../../../src/plugins/data/public'; const { toAbsoluteDates } = search.aggs; @@ -17,7 +17,7 @@ import { LensMultiTable } from '../types'; interface MergeTables { layerIds: string[]; - tables: KibanaDatatable[]; + tables: Datatable[]; } export const mergeTables: ExpressionFunctionDefinition< @@ -38,14 +38,14 @@ export const mergeTables: ExpressionFunctionDefinition< multi: true, }, tables: { - types: ['kibana_datatable'], + types: ['datatable'], help: '', multi: true, }, }, inputTypes: ['kibana_context', 'null'], fn(input, { layerIds, tables }) { - const resultTables: Record = {}; + const resultTables: Record = {}; tables.forEach((table, index) => { resultTables[layerIds[index]] = table; }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts index 4bfd6a4f93c75..43285d657dd40 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts @@ -5,16 +5,16 @@ */ import { renameColumns } from './rename_columns'; -import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; +import { Datatable } from '../../../../../src/plugins/expressions/public'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; describe('rename_columns', () => { it('should rename columns of a given datatable', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', + const input: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, ], rows: [ { a: 1, b: 2 }, @@ -46,10 +46,16 @@ describe('rename_columns', () => { "columns": Array [ Object { "id": "b", + "meta": Object { + "type": "number", + }, "name": "Austrailia", }, Object { "id": "c", + "meta": Object { + "type": "number", + }, "name": "Boomerang", }, ], @@ -71,15 +77,15 @@ describe('rename_columns', () => { "c": 8, }, ], - "type": "kibana_datatable", + "type": "datatable", } `); }); it('should replace "" with a visible value', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'a', name: 'A' }], + const input: Datatable = { + type: 'datatable', + columns: [{ id: 'a', name: 'A', meta: { type: 'string' } }], rows: [{ a: '' }], }; @@ -100,11 +106,11 @@ describe('rename_columns', () => { }); it('should keep columns which are not mapped', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', + const input: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, ], rows: [ { a: 1, b: 2 }, @@ -129,10 +135,16 @@ describe('rename_columns', () => { "columns": Array [ Object { "id": "a", + "meta": Object { + "type": "number", + }, "name": "A", }, Object { "id": "c", + "meta": Object { + "type": "number", + }, "name": "Catamaran", }, ], @@ -154,17 +166,17 @@ describe('rename_columns', () => { "c": 8, }, ], - "type": "kibana_datatable", + "type": "datatable", } `); }); it('should rename date histograms', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', + const input: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'banana per 30 seconds' }, + { id: 'a', name: 'A', meta: { type: 'number' } }, + { id: 'b', name: 'banana per 30 seconds', meta: { type: 'number' } }, ], rows: [ { a: 1, b: 2 }, @@ -189,10 +201,16 @@ describe('rename_columns', () => { "columns": Array [ Object { "id": "a", + "meta": Object { + "type": "number", + }, "name": "A", }, Object { "id": "c", + "meta": Object { + "type": "number", + }, "name": "Apple per 30 seconds", }, ], @@ -214,7 +232,7 @@ describe('rename_columns', () => { "c": 8, }, ], - "type": "kibana_datatable", + "type": "datatable", } `); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts index bf938a3e05ef6..74f143225e293 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts @@ -5,11 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - KibanaDatatableColumn, -} from 'src/plugins/expressions'; +import { ExpressionFunctionDefinition, Datatable, DatatableColumn } from 'src/plugins/expressions'; import { IndexPatternColumn } from './operations'; interface RemapArgs { @@ -20,12 +16,12 @@ export type OriginalColumn = { id: string } & IndexPatternColumn; export const renameColumns: ExpressionFunctionDefinition< 'lens_rename_columns', - KibanaDatatable, + Datatable, RemapArgs, - KibanaDatatable + Datatable > = { name: 'lens_rename_columns', - type: 'kibana_datatable', + type: 'datatable', help: i18n.translate('xpack.lens.functions.renameColumns.help', { defaultMessage: 'A helper to rename the columns of a datatable', }), @@ -38,12 +34,12 @@ export const renameColumns: ExpressionFunctionDefinition< }), }, }, - inputTypes: ['kibana_datatable'], + inputTypes: ['datatable'], fn(data, { idMap: encodedIdMap }) { const idMap = JSON.parse(encodedIdMap) as Record; return { - type: 'kibana_datatable', + type: 'datatable', rows: data.rows.map((row) => { const mappedRow: Record = {}; Object.entries(idMap).forEach(([fromId, toId]) => { @@ -77,7 +73,7 @@ export const renameColumns: ExpressionFunctionDefinition< }, }; -function getColumnName(originalColumn: OriginalColumn, newColumn: KibanaDatatableColumn) { +function getColumnName(originalColumn: OriginalColumn, newColumn: DatatableColumn) { if (originalColumn && originalColumn.operationType === 'date_histogram') { const fieldName = originalColumn.sourceField; diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index 7e80fcc06dff8..88ce026fc2692 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -17,11 +17,11 @@ function sampleArgs() { type: 'lens_multitable', tables: { l1: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'string' } }, + { id: 'b', name: 'b', meta: { type: 'string' } }, + { id: 'c', name: 'c', meta: { type: 'number' } }, ], rows: [{ a: 10110, b: 2, c: 3 }], }, diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 58814f62da60a..6522a4c457949 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -136,8 +136,8 @@ export function MetricChart({ } const value = - column && column.formatHint - ? formatFactory(column.formatHint).convert(row[accessor]) + column && column.meta?.params + ? formatFactory(column.meta?.params).convert(row[accessor]) : Number(Number(row[accessor]).toFixed(3)).toString(); return ( diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index ac952e307758b..8ab1a8b5a58d8 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -31,11 +31,11 @@ describe('PieVisualization component', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [ { a: 6, b: 2, c: 'I', d: 'Row 1' }, @@ -138,14 +138,23 @@ describe('PieVisualization component', () => { "columns": Array [ Object { "id": "a", + "meta": Object { + "type": "number", + }, "name": "a", }, Object { "id": "b", + "meta": Object { + "type": "number", + }, "name": "b", }, Object { "id": "c", + "meta": Object { + "type": "string", + }, "name": "c", }, ], @@ -163,7 +172,7 @@ describe('PieVisualization component', () => { "d": "Row 2", }, ], - "type": "kibana_datatable", + "type": "datatable", }, "value": 6, }, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 8de810f9aa5d3..cb2458a76967c 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -68,7 +68,7 @@ export function PieComponent( if (!hideLabels) { firstTable.columns.forEach((column) => { - formatters[column.id] = props.formatFactory(column.formatHint); + formatters[column.id] = props.formatFactory(column.meta.params); }); } @@ -108,7 +108,7 @@ export function PieComponent( if (hideLabels || d === EMPTY_SLICE) { return ''; } - if (col.formatHint) { + if (col.meta.params) { return formatters[col.id].convert(d) ?? ''; } return String(d); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index 8b94ff3236a44..d9ccda2a99ab2 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaDatatable } from 'src/plugins/expressions/public'; +import { Datatable } from 'src/plugins/expressions/public'; import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; +import { ColumnGroups } from './types'; describe('render helpers', () => { describe('#getSliceValueWithFallback', () => { describe('without fallback', () => { - const columnGroups = [ - { col: { id: 'a', name: 'A' }, metrics: [] }, - { col: { id: 'b', name: 'C' }, metrics: [] }, + const columnGroups: ColumnGroups = [ + { col: { id: 'a', name: 'A', meta: { type: 'string' } }, metrics: [] }, + { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, ]; it('returns the metric when positive number', () => { @@ -20,6 +21,7 @@ describe('render helpers', () => { getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 5 }, columnGroups, { id: 'c', name: 'C', + meta: { type: 'number' }, }) ).toEqual(5); }); @@ -29,6 +31,7 @@ describe('render helpers', () => { getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: -100 }, columnGroups, { id: 'c', name: 'C', + meta: { type: 'number' }, }) ).toEqual(-100); }); @@ -38,15 +41,19 @@ describe('render helpers', () => { getSliceValueWithFallback({ a: 'Cat', b: 'Home', c: 0 }, columnGroups, { id: 'c', name: 'C', + meta: { type: 'number' }, }) ).toEqual(Number.EPSILON); }); }); describe('fallback behavior', () => { - const columnGroups = [ - { col: { id: 'a', name: 'A' }, metrics: [{ id: 'a_subtotal', name: '' }] }, - { col: { id: 'b', name: 'C' }, metrics: [] }, + const columnGroups: ColumnGroups = [ + { + col: { id: 'a', name: 'A', meta: { type: 'string' } }, + metrics: [{ id: 'a_subtotal', name: '', meta: { type: 'number' } }], + }, + { col: { id: 'b', name: 'C', meta: { type: 'string' } }, metrics: [] }, ]; it('falls back to metric from previous column if available', () => { @@ -54,7 +61,7 @@ describe('render helpers', () => { getSliceValueWithFallback( { a: 'Cat', a_subtotal: 5, b: 'Home', c: undefined }, columnGroups, - { id: 'c', name: 'C' } + { id: 'c', name: 'C', meta: { type: 'number' } } ) ).toEqual(5); }); @@ -64,7 +71,7 @@ describe('render helpers', () => { getSliceValueWithFallback( { a: 'Cat', a_subtotal: 0, b: 'Home', c: undefined }, columnGroups, - { id: 'c', name: 'C' } + { id: 'c', name: 'C', meta: { type: 'number' } } ) ).toEqual(Number.EPSILON); }); @@ -74,7 +81,7 @@ describe('render helpers', () => { getSliceValueWithFallback( { a: 'Cat', a_subtotal: undefined, b: 'Home', c: undefined }, columnGroups, - { id: 'c', name: 'C' } + { id: 'c', name: 'C', meta: { type: 'number' } } ) ).toEqual(Number.EPSILON); }); @@ -83,11 +90,11 @@ describe('render helpers', () => { describe('#getFilterContext', () => { it('handles single slice click for single ring', () => { - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'number' } }, ], rows: [ { a: 'Hi', b: 2 }, @@ -108,12 +115,12 @@ describe('render helpers', () => { }); it('handles single slice click with 2 rings', () => { - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - { id: 'c', name: 'C' }, + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'string' } }, + { id: 'c', name: 'C', meta: { type: 'number' } }, ], rows: [ { a: 'Hi', b: 'Two', c: 2 }, @@ -134,12 +141,12 @@ describe('render helpers', () => { }); it('finds right row for multi slice click', () => { - const table: KibanaDatatable = { - type: 'kibana_datatable', + const table: Datatable = { + type: 'datatable', columns: [ - { id: 'a', name: 'A' }, - { id: 'b', name: 'B' }, - { id: 'c', name: 'C' }, + { id: 'a', name: 'A', meta: { type: 'string' } }, + { id: 'b', name: 'B', meta: { type: 'string' } }, + { id: 'c', name: 'C', meta: { type: 'number' } }, ], rows: [ { a: 'Hi', b: 'Two', c: 2 }, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index aafbb477bab22..26b4f9ccda853 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -5,14 +5,14 @@ */ import { Datum, LayerValue } from '@elastic/charts'; -import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; +import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import { ColumnGroups } from './types'; import { LensFilterEvent } from '../types'; export function getSliceValueWithFallback( d: Datum, reverseGroups: ColumnGroups, - metricColumn: KibanaDatatableColumn + metricColumn: DatatableColumn ) { if (typeof d[metricColumn.id] === 'number' && d[metricColumn.id] !== 0) { return d[metricColumn.id]; @@ -27,7 +27,7 @@ export function getSliceValueWithFallback( export function getFilterContext( clickedLayers: LayerValue[], layerColumnIds: string[], - table: KibanaDatatable + table: Datatable ): LensFilterEvent['data'] { const matchingIndex = table.rows.findIndex((row) => clickedLayers.every((layer, index) => { diff --git a/x-pack/plugins/lens/public/pie_visualization/types.ts b/x-pack/plugins/lens/public/pie_visualization/types.ts index 603c80aa00066..0596e54870a94 100644 --- a/x-pack/plugins/lens/public/pie_visualization/types.ts +++ b/x-pack/plugins/lens/public/pie_visualization/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaDatatableColumn } from 'src/plugins/expressions/public'; +import { DatatableColumn } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; export interface SharedLayerState { @@ -40,6 +40,6 @@ export interface PieExpressionProps { } export type ColumnGroups = Array<{ - col: KibanaDatatableColumn; - metrics: KibanaDatatableColumn[]; + col: DatatableColumn; + metrics: DatatableColumn[]; }>; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 2b9ca5a2425f8..e70436163b23d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -11,7 +11,7 @@ import { SavedObjectReference } from 'kibana/public'; import { ExpressionRendererEvent, IInterpreterRenderHandlers, - KibanaDatatable, + Datatable, SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; import { DragContextState } from './drag_drop'; @@ -304,7 +304,7 @@ export interface OperationMetadata { export interface LensMultiTable { type: 'lens_multitable'; - tables: Record; + tables: Record; dateRange?: { fromDate: Date; toDate: Date; diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts index 170579b7c551b..59b81fd3d1136 100644 --- a/x-pack/plugins/lens/public/utils.test.ts +++ b/x-pack/plugins/lens/public/utils.test.ts @@ -6,10 +6,12 @@ import { LensFilterEvent } from './types'; import { desanitizeFilterContext } from './utils'; +import { Datatable } from '../../../../src/plugins/expressions/common'; describe('desanitizeFilterContext', () => { it(`When filtered value equals '(empty)' replaces it with '' in table and in value.`, () => { - const table = { + const table: Datatable = { + type: 'datatable', rows: [ { 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, @@ -35,14 +37,17 @@ describe('desanitizeFilterContext', () => { { id: 'f903668f-1175-4705-a5bd-713259d10326', name: 'order_date per 30 seconds', + meta: { type: 'date' }, }, { id: '5d5446b2-72e8-4f86-91e0-88380f0fa14c', name: 'Top values of customer_phone', + meta: { type: 'string' }, }, { id: '9f0b6f88-c399-43a0-a993-0ad943c9af25', name: 'Count of records', + meta: { type: 'number' }, }, ], }; @@ -102,6 +107,7 @@ describe('desanitizeFilterContext', () => { }, ], columns: table.columns, + type: 'datatable', }, }, ], diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 171707dcb9d26..0461e600d2b4c 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -14,7 +14,7 @@ export const desanitizeFilterContext = ( const emptyTextValue = i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { defaultMessage: '(empty)', }); - return { + const result: LensFilterEvent['data'] = { ...context, data: context.data.map((point) => point.value === emptyTextValue @@ -36,4 +36,8 @@ export const desanitizeFilterContext = ( : point ), }; + if (context.timeFieldName) { + result.timeFieldName = context.timeFieldName; + } + return result; }; diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts index 15c08d17e49c6..a823a6370270d 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.test.ts @@ -5,13 +5,13 @@ */ import { LayerArgs } from './types'; -import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; +import { Datatable } from '../../../../../src/plugins/expressions/public'; import { getAxesConfiguration } from './axes_configuration'; describe('axes_configuration', () => { - const tables: Record = { + const tables: Record = { first: { - type: 'kibana_datatable', + type: 'datatable', rows: [ { xAccessorId: 1585758120000, @@ -99,48 +99,60 @@ describe('axes_configuration', () => { id: 'xAccessorId', name: 'order_date per minute', meta: { - type: 'date_histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'order_date', - timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: '1m', - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, }, + params: { params: { id: 'date', params: { pattern: 'HH:mm' } } }, }, - formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, }, { id: 'splitAccessorId', name: 'Top values of category.keyword', meta: { - type: 'terms', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'category.keyword', - orderBy: 'yAccessorId', - order: 'desc', - size: 3, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, }, - }, - formatHint: { - id: 'terms', params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - parsedUrl: { - origin: 'http://localhost:5601', - pathname: '/jiy/app/kibana', - basePath: '/jiy', + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, }, }, }, @@ -149,41 +161,57 @@ describe('axes_configuration', () => { id: 'yAccessorId', name: 'Count of records', meta: { - type: 'count', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'count', + }, + params: { id: 'number' }, }, - formatHint: { id: 'number' }, }, { id: 'yAccessorId2', name: 'Other column', meta: { - type: 'average', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'bytes' }, }, - formatHint: { id: 'bytes' }, }, { id: 'yAccessorId3', name: 'Other column', meta: { - type: 'average', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'currency' }, }, - formatHint: { id: 'currency' }, }, { id: 'yAccessorId4', name: 'Other column', meta: { - type: 'average', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'average', + }, + params: { id: 'currency' }, }, - formatHint: { id: 'currency' }, }, ], }, diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts index 876baaabb57c5..3c312abf1fd91 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -5,10 +5,7 @@ */ import { LayerConfig } from './types'; -import { - KibanaDatatable, - SerializedFieldFormat, -} from '../../../../../src/plugins/expressions/public'; +import { Datatable, SerializedFieldFormat } from '../../../../../src/plugins/expressions/public'; import { IFieldFormat } from '../../../../../src/plugins/data/public'; interface FormattedMetric { @@ -34,7 +31,7 @@ export function isFormatterCompatible( export function getAxesConfiguration( layers: LayerConfig[], shouldRotate: boolean, - tables?: Record, + tables?: Record, formatFactory?: (mapping: SerializedFieldFormat) => IFieldFormat ): GroupsConfiguration { const series: { auto: FormattedMetric[]; left: FormattedMetric[]; right: FormattedMetric[] } = { @@ -50,7 +47,7 @@ export function getAxesConfiguration( layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode || 'auto'; let formatter: SerializedFieldFormat = table?.columns.find((column) => column.id === accessor) - ?.formatHint || { id: 'number' }; + ?.meta?.params || { id: 'number' }; if (layer.seriesType.includes('percentage') && formatter.id !== 'percent') { formatter = { id: 'percent', 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 e7da850983de6..9e937399a7969 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/charts'; import { xyChart, XYChart } from './expression'; import { LensMultiTable } from '../types'; -import { KibanaDatatable, KibanaDatatableRow } from '../../../../../src/plugins/expressions/public'; +import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public'; import React from 'react'; import { shallow } from 'enzyme'; import { @@ -46,7 +46,7 @@ const dateHistogramData: LensMultiTable = { type: 'lens_multitable', tables: { timeLayer: { - type: 'kibana_datatable', + type: 'datatable', rows: [ { xAccessorId: 1585758120000, @@ -104,48 +104,60 @@ const dateHistogramData: LensMultiTable = { id: 'xAccessorId', name: 'order_date per minute', meta: { - type: 'date_histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'order_date', - timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: '1m', - drop_partials: false, - min_doc_count: 0, - extended_bounds: {}, + type: 'date', + field: 'order_date', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'date_histogram', + params: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, }, + params: { id: 'date', params: { pattern: 'HH:mm' } }, }, - formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, }, { id: 'splitAccessorId', name: 'Top values of category.keyword', meta: { - type: 'terms', - indexPatternId: 'indexPatternId', - aggConfigParams: { - field: 'category.keyword', - orderBy: 'yAccessorId', - order: 'desc', - size: 3, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + type: 'string', + field: 'category.keyword', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + type: 'terms', + params: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, }, - }, - formatHint: { - id: 'terms', params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - parsedUrl: { - origin: 'http://localhost:5601', - pathname: '/jiy/app/kibana', - basePath: '/jiy', + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, }, }, }, @@ -154,11 +166,15 @@ const dateHistogramData: LensMultiTable = { id: 'yAccessorId', name: 'Count of records', meta: { - type: 'count', - indexPatternId: 'indexPatternId', - aggConfigParams: {}, + type: 'number', + source: 'esaggs', + index: 'indexPatternId', + sourceParams: { + indexPatternId: 'indexPatternId', + params: {}, + }, + params: { id: 'number' }, }, - formatHint: { id: 'number' }, }, ], }, @@ -181,22 +197,30 @@ const dateHistogramLayer: LayerArgs = { accessors: ['yAccessorId'], }; -const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({ - type: 'kibana_datatable', +const createSampleDatatableWithRows = (rows: DatatableRow[]): Datatable => ({ + type: 'datatable', columns: [ { id: 'a', name: 'a', - formatHint: { id: 'number', params: { pattern: '0,0.000' } }, + meta: { type: 'number', params: { id: 'number', params: { pattern: '0,0.000' } } }, + }, + { + id: 'b', + name: 'b', + meta: { type: 'number', params: { id: 'number', params: { pattern: '000,0' } } }, }, - { id: 'b', name: 'b', formatHint: { id: 'number', params: { pattern: '000,0' } } }, { id: 'c', name: 'c', - formatHint: { id: 'string' }, - meta: { type: 'date-histogram', aggConfigParams: { interval: 'auto' } }, + meta: { + type: 'date', + field: 'order_date', + sourceParams: { type: 'date-histogram', params: { interval: 'auto' } }, + params: { id: 'string' }, + }, }, - { id: 'd', name: 'ColD', formatHint: { id: 'string' } }, + { id: 'd', name: 'ColD', meta: { type: 'string' } }, ], rows, }); @@ -347,12 +371,12 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, - { id: 'd', name: 'd' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + { id: 'd', name: 'd', meta: { type: 'string' } }, ], rows: [ { a: 1, b: 2, c: 'I', d: 'Row 1' }, @@ -365,12 +389,12 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, - { id: 'd', name: 'd', formatHint: { id: 'custom' } }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, + { id: 'd', name: 'd', meta: { type: 'string', params: { id: 'custom' } } }, ], rows: [ { a: 1, b: 2, c: 'I', d: 'Row 1' }, @@ -542,12 +566,12 @@ describe('xy_expression', () => { ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); }); test('it generates correct xDomain for a layer with single value and layer with multiple value data (1-n)', () => { const data: LensMultiTable = { @@ -625,12 +649,12 @@ describe('xy_expression', () => { ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` - Object { - "max": 1546491600000, - "min": 1546405200000, - "minInterval": undefined, - } - `); + Object { + "max": 1546491600000, + "min": 1546405200000, + "minInterval": undefined, + } + `); }); }); @@ -792,7 +816,7 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { numberLayer: { - type: 'kibana_datatable', + type: 'datatable', rows: [ { xAccessorId: 5, @@ -815,10 +839,12 @@ describe('xy_expression', () => { { id: 'xAccessorId', name: 'bytes', + meta: { type: 'number' }, }, { id: 'yAccessorId', name: 'Count of records', + meta: { type: 'number' }, }, ], }, @@ -1737,11 +1763,11 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [ { a: undefined, b: 2, c: 'I', d: 'Row 1' }, @@ -1749,11 +1775,11 @@ describe('xy_expression', () => { ], }, second: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [ { a: undefined, b: undefined, c: undefined }, @@ -1831,11 +1857,11 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'number' } }, ], rows: [ { a: 0, b: 2, c: 5 }, @@ -1903,11 +1929,11 @@ describe('xy_expression', () => { type: 'lens_multitable', tables: { first: { - type: 'kibana_datatable', + type: 'datatable', columns: [ - { id: 'a', name: 'a' }, - { id: 'b', name: 'b' }, - { id: 'c', name: 'c' }, + { id: 'a', name: 'a', meta: { type: 'number' } }, + { id: 'b', name: 'b', meta: { type: 'number' } }, + { id: 'c', name: 'c', meta: { type: 'string' } }, ], rows: [{ a: 1, b: 5, c: 'J' }], }, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index dad1d31ced71f..4a2c13e1e3520 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -26,7 +26,8 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, ExpressionValueSearchContext, - KibanaDatatable, + Datatable, + DatatableRow, } from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -271,7 +272,7 @@ export function XYChart({ const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find( ({ id }) => id === filteredLayers[0].xAccessor ); - const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint); + const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params); const layersAlreadyFormatted: Record = {}; // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers const safeXAccessorLabelRenderer = (value: unknown): string => @@ -330,8 +331,8 @@ export function XYChart({ // add minInterval only for single point in domain if (data.dateRange && isSingleTimestampInXDomain()) { - if (xAxisColumn?.meta?.aggConfigParams?.interval !== 'auto') - return parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds(); + const params = xAxisColumn?.meta?.sourceParams?.params as Record; + if (params?.interval !== 'auto') return parseInterval(params?.interval)?.asMilliseconds(); const { fromDate, toDate } = data.dateRange; const duration = moment(toDate).diff(moment(fromDate)); @@ -417,8 +418,9 @@ export function XYChart({ const xAxisColumnIndex = table.columns.findIndex( (el) => el.id === filteredLayers[0].xAccessor ); + const timeFieldName = isTimeViz - ? table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field + ? table.columns[xAxisColumnIndex]?.meta?.field : undefined; const context: LensBrushEvent['data'] = { @@ -471,8 +473,7 @@ export function XYChart({ }); } - const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta - ?.aggConfigParams?.field; + const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field; const timeFieldName = xDomain && xAxisFieldName; const context: LensFilterEvent['data'] = { @@ -552,14 +553,14 @@ export function XYChart({ // what if row values are not primitive? That is the case of, for instance, Ranges // remaps them to their serialized version with the formatHint metadata // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on - const tableConverted: KibanaDatatable = { + const tableConverted: Datatable = { ...table, - rows: table.rows.map((row) => { + rows: table.rows.map((row: DatatableRow) => { const newRow = { ...row }; for (const column of table.columns) { const record = newRow[column.id]; if (record && !isPrimitive(record)) { - newRow[column.id] = formatFactory(column.formatHint).convert(record); + newRow[column.id] = formatFactory(column.meta.params).convert(record); } } return newRow; @@ -634,7 +635,7 @@ export function XYChart({ }, }, name(d) { - const splitHint = table.columns.find((col) => col.id === splitAccessor)?.formatHint; + const splitHint = table.columns.find((col) => col.id === splitAccessor)?.meta?.params; // For multiple y series, the name of the operation is used on each, either: // * Key - Y name From 3704b1d301c16f0d31a0980f4ac4aa1dc302b564 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 16 Oct 2020 13:15:47 -0400 Subject: [PATCH 64/79] [Actions] Back Button on Add Connector Flyout (#80160) * Adding back button to add connector flyout * Adding tests * Adding tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../connector_add_flyout.test.tsx | 2 ++ .../connector_add_flyout.tsx | 33 ++++++++++++++----- .../apps/triggers_actions_ui/connectors.ts | 4 +++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index f60199bc47f4b..0863465833c0b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -75,6 +75,8 @@ describe('connector_add_flyout', () => { ); expect(wrapper.find('ActionTypeMenu')).toHaveLength(1); expect(wrapper.find(`[data-test-subj="${actionType.id}-card"]`).exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="cancelButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="backButton"]').exists()).toBeFalsy(); }); it('renders banner with subscription links when gold features are disabled due to licensing ', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 9bb9d07307e13..060a751677de0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -251,14 +251,31 @@ export const ConnectorAddFlyout = ({ - - {i18n.translate( - 'xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - )} - + {!actionType ? ( + + {i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + ) : ( + { + setActionType(undefined); + setConnector(initialConnector); + }} + > + {i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel', + { + defaultMessage: 'Back', + } + )} + + )} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index f56e0e2629d40..f55114cf11d14 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -40,6 +40,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + await testSubjects.click('.index-card'); + + await find.clickByCssSelector('[data-test-subj="backButton"]'); + await testSubjects.click('.slack-card'); await testSubjects.setValue('nameInput', connectorName); From 7f278416d88a2597772a583579238e730395cccb Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 16 Oct 2020 10:21:29 -0700 Subject: [PATCH 65/79] Add catch for Enterprise Search sending back a 401 response instead of redirect (#80757) - apparently this changed at some point between 7.9 and 7.10 --- .../lib/enterprise_search_request_handler.test.ts | 4 ++++ .../server/lib/enterprise_search_request_handler.ts | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index d8cb24b34496b..e55f997a6b51b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -325,6 +325,10 @@ describe('EnterpriseSearchRequestHandler', () => { expect(mockLogger.error).toHaveBeenCalled(); }); + it('errors when receiving a 401 response', async () => { + EnterpriseSearchAPI.mockReturn({}, { status: 401 }); + }); + it('errors when redirected to /login', async () => { EnterpriseSearchAPI.mockReturn({}, { url: 'http://localhost:3002/login' }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index cb28cd2b90f4d..ad6d936ac0c36 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -84,8 +84,12 @@ export class EnterpriseSearchRequestHandler { // Handle response headers this.setResponseHeaders(apiResponse); - // Handle authentication redirects - if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { + // Handle unauthenticated users / authentication redirects + if ( + apiResponse.status === 401 || + apiResponse.url.endsWith('/login') || + apiResponse.url.endsWith('/ent/select') + ) { return this.handleAuthenticationError(response); } @@ -213,6 +217,10 @@ export class EnterpriseSearchRequestHandler { return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); } + /** + * Note: Kibana auto logs users out when it receives a 401 response, so we want to catch and + * return 401 responses from Enterprise Search as a 502 so Kibana sessions aren't interrupted + */ handleAuthenticationError(response: KibanaResponseFactory) { const errorMessage = 'Cannot authenticate Enterprise Search user'; From 7ffb317706771dab8c5a81fe76db4bdc3a057fbe Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 16 Oct 2020 10:43:00 -0700 Subject: [PATCH 66/79] [Reporting] Add contextual documentation for CSV Max Bytes setting (#80782) * [Reporting] Add contextual documentation for CSV Max Bytes setting * Update reporting-settings.asciidoc * updates per feedback --- docs/settings/reporting-settings.asciidoc | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index adfc3964d4204..a948bb56221aa 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -10,6 +10,7 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: * <> * <> * <> +* <> [float] [[general-reporting-settings]] @@ -65,7 +66,7 @@ proxy host requires that the {kib} server has network access to the proxy. [NOTE] ============ -Reporting authenticates requests on the Kibana page only when the hostname matches the +Reporting authenticates requests on the {kib} page only when the hostname matches the <> setting. Therefore Reporting would fail if the set value redirects to another server. For that reason, `"0"` is an invalid setting because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`. @@ -214,6 +215,23 @@ a| `xpack.reporting.capture.browser` | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to prevent large exports from causing performance and storage issues. Can be specified as number of bytes. Defaults to `10mb`. +|=== + +[NOTE] +============ +Setting `xpack.reporting.csv.maxSizeBytes` much larger than the default 10 MB limit has the potential to negatively affect the +performance of {kib} and your {es} cluster. There is no enforced maximum for this setting, but a reasonable maximum value depends +on multiple factors: + +* The `http.max_content_length` setting in {es}. +* Network proxies, which are often configured by default to block large requests with a 413 error. +* The amount of memory available to the {kib} server, which limits the size of CSV data that must be held temporarily. + +For information about {kib} memory limits, see <>. +============ + +[cols="2*<"] +|=== | `xpack.reporting.csv.scroll.size` | Number of documents retrieved from {es} for each scroll iteration during a CSV From c3b1b179166edfc6a9052885817e8f1712526288 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 16 Oct 2020 10:43:17 -0700 Subject: [PATCH 67/79] [Reporting] Document Network Policy configuration (#80431) * [Reporting] Document Network Policy configuration * Apply suggestions from code review Co-authored-by: Larry Gregory * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * remove detail about policy acting on responses * Update docs/user/reporting/network-policy.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * lowercase network policy * typo Co-authored-by: Larry Gregory Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/settings/reporting-settings.asciidoc | 5 ++ .../reporting/configuring-reporting.asciidoc | 1 + docs/user/reporting/network-policy.asciidoc | 71 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 docs/user/reporting/network-policy.asciidoc diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index a948bb56221aa..d44c42db92f41 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -266,6 +266,11 @@ For information about {kib} memory limits, see <> setting. Defaults to `.reporting`. +| `xpack.reporting.capture.networkPolicy` + | Capturing a screenshot from a {kib} page involves sending out requests for all the linked web assets. For example, a Markdown + visualization can show an image from a remote server. You can configure what type of requests to allow or filter by setting a + <> for Reporting. + | `xpack.reporting.roles.allow` | Specifies the roles in addition to superusers that can use reporting. Defaults to `[ "reporting_user" ]`. + diff --git a/docs/user/reporting/configuring-reporting.asciidoc b/docs/user/reporting/configuring-reporting.asciidoc index 6a0c44cf4c2a4..a8b76f36b9a84 100644 --- a/docs/user/reporting/configuring-reporting.asciidoc +++ b/docs/user/reporting/configuring-reporting.asciidoc @@ -75,3 +75,4 @@ to point to a proxy host requires that the Kibana server has network access to the proxy. include::{kib-repo-dir}/user/security/reporting.asciidoc[] +include::network-policy.asciidoc[] diff --git a/docs/user/reporting/network-policy.asciidoc b/docs/user/reporting/network-policy.asciidoc new file mode 100644 index 0000000000000..782473a3b0f18 --- /dev/null +++ b/docs/user/reporting/network-policy.asciidoc @@ -0,0 +1,71 @@ +[role="xpack"] +[[reporting-network-policy]] +=== Restrict requests with a Reporting network policy + +When Reporting generates PDF reports, it uses the Chromium browser to fully load the {kib} page on the server. This +potentially involves sending requests to external hosts. For example, a request might go to an external image server to show a +field formatted as an image, or to show an image in a Markdown visualization. + +If the Chromium browser is asked to send a request that violates the network policy, Reporting stops processing the page +before the request goes out, and the report is marked as a failure. Additional information about the event is in +the Kibana server logs. + +[NOTE] +============ +{kib} installations are not designed to be publicly accessible over the Internet. The Reporting network policy and other capabilities +of the Elastic Stack security features do not change this condition. +============ + +==== Configure a Reporting network policy + +You configure the network policy by specifying the `xpack.reporting.capture.networkPolicy.rules` setting in `kibana.yml`. A policy is specified as +an array of objects that describe what to allow or deny based on a host or protocol. If a host or protocol +is not specified, the rule matches any host or protocol. + +The rule objects are evaluated sequentially from the beginning to the end of the array, and continue until there is a matching rule. +If no rules allow a request, the request is denied. + +[source,yaml] +------------------------------------------------------- +# Only allow requests to placeholder.com +xpack.reporting.capture.networkPolicy: + rules: [ { allow: true, host: "placeholder.com" } ] +------------------------------------------------------- + +[source,yaml] +------------------------------------------------------- +# Only allow requests to https://placeholder.com +xpack.reporting.capture.networkPolicy: + rules: [ { allow: true, host: "placeholder.com", protocol: "https:" } ] +------------------------------------------------------- + +A final `allow` rule with no host or protocol will allow all requests that are not explicitly denied. + +[source,yaml] +------------------------------------------------------- +# Denies requests from http://placeholder.com, but anything else is allowed. +xpack.reporting.capture.networkPolicy: + rules: [{ allow: false, host: "placeholder.com", protocol: "http:" }, { allow: true }]; +------------------------------------------------------- + +A network policy can be composed of multiple rules. + +[source,yaml] +------------------------------------------------------- +# Allow any request to http://placeholder.com but for any other host, https is required +xpack.reporting.capture.networkPolicy + rules: [ + { allow: true, host: "placeholder.com", protocol: "http:" }, + { allow: true, protocol: "https:" }, + ] +------------------------------------------------------- + +[NOTE] +============ +The `file:` protocol is always denied, even if no network policy is configured. +============ + +==== Disable a Reporting network policy + +You can use the `xpack.reporting.capture.networkPolicy.enabled: false` setting to disable the network policy feature. The default for +this configuration property is `true`, so it is not necessary to explicitly enable it. From 02d850f4a8298a52f568a6a9f89f5e89ea28142d Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 16 Oct 2020 10:47:05 -0700 Subject: [PATCH 68/79] Move renderHeaderActions back into mount useEffect + update tests (#80861) --- .../public/applications/workplace_search/index.test.tsx | 1 + .../public/applications/workplace_search/index.tsx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 6aa4cf59ab46c..25544b4a9bb68 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -76,6 +76,7 @@ describe('WorkplaceSearchConfigured', () => { shallow(); expect(initializeAppData).not.toHaveBeenCalled(); + expect(mockKibanaValues.renderHeaderActions).not.toHaveBeenCalled(); }); it('renders ErrorState', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index a3c7f7d48a612..e22b9c6282f95 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -38,11 +38,10 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { useEffect(() => { if (!hasInitialized) { initializeAppData(props); + renderHeaderActions(WorkplaceSearchHeaderActions); } }, [hasInitialized]); - renderHeaderActions(WorkplaceSearchHeaderActions); - return ( From 9edae0c84ad00f2d7fca10288615dd21752cda06 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Fri, 16 Oct 2020 14:06:50 -0400 Subject: [PATCH 69/79] [Security_Solution][Resolver] Promote z-index on node labels (#80854) --- .../public/resolver/view/process_event_dot.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 7968b4a3a1fd0..7a3657fe93514 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -62,7 +62,7 @@ const StyledDescriptionText = styled.div` text-align: left; text-transform: uppercase; width: fit-content; - z-index: 40; + z-index: 45; `; const StyledOuterGroup = styled.g` @@ -393,7 +393,7 @@ const UnstyledProcessEventDot = React.memo( backgroundColor: colorMap.resolverBackground, alignSelf: 'flex-start', padding: 0, - zIndex: 40, + zIndex: 45, }} > Date: Fri, 16 Oct 2020 15:24:05 -0400 Subject: [PATCH 70/79] [Uptime] Add client-side unit tests for remaining synthetics code (#80215) * Test remaining branches in synthetics components. * Fix TS errors. * PR feedback. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__tests__/browser_expanded_row.test.tsx | 25 +- .../__tests__/console_event.test.tsx | 107 ++++++++ .../console_output_event_list.test.tsx | 150 ++++++++++ .../__tests__/empty_journey.test.tsx | 94 +++++++ .../__tests__/executed_journey.test.tsx | 259 ++++++++++++++++++ .../synthetics/browser_expanded_row.tsx | 4 +- .../synthetics/console_output_event_list.tsx | 10 +- .../monitor/synthetics/empty_journey.tsx | 4 +- .../monitor/synthetics/executed_journey.tsx | 16 +- 9 files changed, 655 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx index 191632d6ab713..07c3afdf50eea 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/browser_expanded_row.test.tsx @@ -28,7 +28,7 @@ describe('BrowserExpandedRowComponent', () => { it('returns empty step state when no journey', () => { expect(shallowWithIntl()).toMatchInlineSnapshot( - `` + `` ); }); @@ -43,7 +43,7 @@ describe('BrowserExpandedRowComponent', () => { }} /> ) - ).toMatchInlineSnapshot(``); + ).toMatchInlineSnapshot(``); }); it('displays loading spinner when loading', () => { @@ -111,6 +111,27 @@ describe('BrowserExpandedRowComponent', () => { `); }); + it('handles case where synth type is somehow missing', () => { + expect( + shallowWithIntl( + + ) + ).toMatchInlineSnapshot(`""`); + }); + it('renders console output step list when only console steps are present', () => { expect( shallowWithIntl( diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx new file mode 100644 index 0000000000000..ad905076a06cd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_event.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ConsoleEvent } from '../console_event'; + +describe('ConsoleEvent component', () => { + it('renders danger color for errors', () => { + expect( + shallowWithIntl( + + ) + ).toMatchInlineSnapshot(` + + + 123 + + + stderr + + + catastrophic error + + + `); + }); + + it('uses default color for non-errors', () => { + expect( + shallowWithIntl( + + ) + ).toMatchInlineSnapshot(` + + + 123 + + + cmd/status + + + not a catastrophic error + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx new file mode 100644 index 0000000000000..776fd0a5fb94d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/console_output_event_list.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ConsoleOutputEventList } from '../console_output_event_list'; + +describe('ConsoleOutputEventList component', () => { + it('renders a component per console event', () => { + expect( + shallowWithIntl( + + ).find('EuiCodeBlock') + ).toMatchInlineSnapshot(` + + + + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx new file mode 100644 index 0000000000000..0157229b3c212 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/empty_journey.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { EmptyJourney } from '../empty_journey'; + +describe('EmptyJourney component', () => { + it('omits check group element when undefined', () => { + expect(shallowWithIntl()).toMatchInlineSnapshot(` + +

+ +

+

+ +

+ + } + iconType="cross" + title={ +

+ +

+ } + /> + `); + }); + + it('includes check group element when present', () => { + expect(shallowWithIntl()).toMatchInlineSnapshot(` + +

+ +

+

+ + check_group + , + } + } + /> +

+

+ +

+ + } + iconType="cross" + title={ +

+ +

+ } + /> + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx new file mode 100644 index 0000000000000..5ab815a3c0b5d --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/__tests__/executed_journey.test.tsx @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ExecutedJourney } from '../executed_journey'; +import { Ping } from '../../../../../common/runtime_types'; + +const MONITOR_BOILERPLATE = { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', +}; + +describe('ExecutedJourney component', () => { + let steps: Ping[]; + + beforeEach(() => { + steps = [ + { + docId: '1', + timestamp: '123', + monitor: MONITOR_BOILERPLATE, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + }, + }, + { + docId: '2', + timestamp: '124', + monitor: MONITOR_BOILERPLATE, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + }, + }, + ]; + }); + + it('creates expected message for all failed', () => { + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - all failed or skipped +

+
+ `); + }); + + it('creates expected message for all succeeded', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'succeeded'; + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - all succeeded +

+
+ `); + }); + + it('creates appropriate message for mixed results', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - 1 succeeded +

+
+ `); + }); + + it('tallies skipped steps', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'skipped'; + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - 1 succeeded +

+
+ `); + }); + + it('uses appropriate count when non-step/end steps are included', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps.push({ + docId: '3', + timestamp: '125', + monitor: MONITOR_BOILERPLATE, + synthetics: { + type: 'stderr', + error: { + message: `there was an error, that's all we know`, + stack: 'your.error.happened.here', + }, + }, + }); + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` + +

+ +

+

+ 2 Steps - 1 succeeded +

+
+ `); + }); + + it('renders a component per step', () => { + expect( + shallowWithIntl( + + ).find('EuiFlexGroup') + ).toMatchInlineSnapshot(` + + + + + `); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx index 2546c5fb9a5d8..4b7461604b301 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx @@ -11,7 +11,7 @@ import { Ping } from '../../../../common/runtime_types'; import { getJourneySteps } from '../../../state/actions/journey'; import { JourneyState } from '../../../state/reducers/journey'; import { journeySelector } from '../../../state/selectors'; -import { EmptyStepState } from './empty_journey'; +import { EmptyJourney } from './empty_journey'; import { ExecutedJourney } from './executed_journey'; import { ConsoleOutputEventList } from './console_output_event_list'; @@ -51,7 +51,7 @@ export const BrowserExpandedRowComponent: FC = ({ checkGroup, jo } if (!journey || journey.steps.length === 0) { - return ; + return ; } if (journey.steps.some(stepEnd)) return ; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx index 9159c61532f15..8f3d6cec9932e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx @@ -7,6 +7,7 @@ import { EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; +import { Ping } from '../../../../common/runtime_types'; import { JourneyState } from '../../../state/reducers/journey'; import { ConsoleEvent } from './console_event'; @@ -14,6 +15,11 @@ interface Props { journey: JourneyState; } +const isConsoleStep = (step: Ping) => + step.synthetics?.type === 'stderr' || + step.synthetics?.type === 'stdout' || + step.synthetics?.type === 'cmd/status'; + export const ConsoleOutputEventList: FC = ({ journey }) => (
@@ -33,8 +39,8 @@ export const ConsoleOutputEventList: FC = ({ journey }) => (

- {journey.steps.map((consoleEvent) => ( - + {journey.steps.filter(isConsoleStep).map((consoleEvent) => ( + ))}
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx index b6fead2bbbe09..4076d9ff7dfdd 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx @@ -8,11 +8,11 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -interface EmptyStepStateProps { +interface Props { checkGroup?: string; } -export const EmptyStepState: FC = ({ checkGroup }) => ( +export const EmptyJourney: FC = ({ checkGroup }) => ( = ({ journey }) => (

{statusMessage( - journey.steps.reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) + journey.steps + .filter(isStepEnd) + .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) )}

- {journey.steps - .filter((step) => step.synthetics?.type === 'step/end') - .map((step, index) => ( - - ))} + {journey.steps.filter(isStepEnd).map((step, index) => ( + + ))}
); From bc8a1dac99af3ea16bbcf5f2825d0f90724fa3b3 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Fri, 16 Oct 2020 20:40:38 +0100 Subject: [PATCH 71/79] ECS audit logging (#74640) * ECS audit logging * Apply suggestions from code review Co-authored-by: Larry Gregory * Update x-pack/plugins/security/server/authentication/audit_events.ts Co-authored-by: Larry Gregory * Update docs/settings/security-settings.asciidoc Co-authored-by: Larry Gregory * remove audit trail service from core * fix test * Updated docs and added beta warning * Added dev docs * Tweaks * Plugin list changes * Apply suggestions from technical writers Co-authored-by: Kaarina Tungseth * Added docs suggestion * Added api integration tests * Added suggestions from platform team * Update x-pack/plugins/security/server/audit/audit_service.test.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update x-pack/plugins/security/server/audit/audit_service.test.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update x-pack/plugins/security/server/audit/audit_service.test.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update docs/user/security/audit-logging.asciidoc Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update docs/settings/security-settings.asciidoc Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Update x-pack/plugins/security/server/config.ts Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> * Added suggestions from PR * Grouped events table * Update x-pack/plugins/security/server/audit/audit_events.ts Co-authored-by: Larry Gregory * Update x-pack/plugins/security/server/audit/audit_events.ts Co-authored-by: Larry Gregory * Fixed ECS version number in docs Co-authored-by: Larry Gregory * Added suggestions from code review * Removed beta * Added suggestions from code review Co-authored-by: Larry Gregory Co-authored-by: Kaarina Tungseth Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 7 +- ...ibana-plugin-core-server.auditableevent.md | 25 - ...ugin-core-server.auditableevent.message.md | 11 - ...-plugin-core-server.auditableevent.type.md | 11 - .../kibana-plugin-core-server.auditor.add.md | 36 -- .../kibana-plugin-core-server.auditor.md | 21 - ...ugin-core-server.auditor.withauditscope.md | 24 - ...gin-core-server.auditorfactory.asscoped.md | 22 - ...ibana-plugin-core-server.auditorfactory.md | 20 - ...bana-plugin-core-server.audittrailsetup.md | 18 - ...in-core-server.audittrailsetup.register.md | 24 - ...bana-plugin-core-server.audittrailstart.md | 11 - ...plugin-core-server.coresetup.audittrail.md | 13 - .../kibana-plugin-core-server.coresetup.md | 1 - ...plugin-core-server.corestart.audittrail.md | 13 - .../kibana-plugin-core-server.corestart.md | 1 - ...erver.legacyclusterclient._constructor_.md | 3 +- ...-plugin-core-server.legacyclusterclient.md | 2 +- ...legacyscopedclusterclient._constructor_.md | 3 +- ...n-core-server.legacyscopedclusterclient.md | 2 +- .../core/server/kibana-plugin-core-server.md | 7 +- ...-core-server.requesthandlercontext.core.md | 1 - ...lugin-core-server.requesthandlercontext.md | 4 +- docs/settings/security-settings.asciidoc | 113 +++- docs/user/security/audit-logging.asciidoc | 137 ++++- .../audit_trail/audit_trail_service.mock.ts | 58 -- .../audit_trail/audit_trail_service.test.ts | 99 ---- .../server/audit_trail/audit_trail_service.ts | 69 --- src/core/server/audit_trail/index.ts | 21 - src/core/server/audit_trail/types.ts | 76 --- .../server/core_route_handler_context.test.ts | 35 -- src/core/server/core_route_handler_context.ts | 10 - .../elasticsearch_service.test.ts | 17 +- .../elasticsearch/elasticsearch_service.ts | 17 +- .../legacy/cluster_client.test.ts | 143 +---- .../elasticsearch/legacy/cluster_client.ts | 16 +- .../legacy/scoped_cluster_client.test.ts | 45 -- .../legacy/scoped_cluster_client.ts | 18 +- src/core/server/index.ts | 9 - src/core/server/internal_types.ts | 3 - src/core/server/legacy/legacy_service.test.ts | 2 - src/core/server/legacy/legacy_service.ts | 2 - src/core/server/mocks.ts | 6 - src/core/server/plugins/plugin_context.ts | 2 - src/core/server/server.api.md | 41 +- src/core/server/server.test.mocks.ts | 6 - src/core/server/server.test.ts | 8 - src/core/server/server.ts | 14 +- .../server/authorization/audit_logger.ts | 6 +- .../server/alerts_client_factory.test.ts | 4 +- .../server/authorization/audit_logger.ts | 6 +- x-pack/plugins/audit_trail/kibana.json | 10 - .../server/client/audit_trail_client.test.ts | 65 --- .../server/client/audit_trail_client.ts | 47 -- .../plugins/audit_trail/server/config.test.ts | 56 -- x-pack/plugins/audit_trail/server/config.ts | 22 - x-pack/plugins/audit_trail/server/index.ts | 13 - .../plugins/audit_trail/server/plugin.test.ts | 125 ---- x-pack/plugins/audit_trail/server/plugin.ts | 97 ---- x-pack/plugins/audit_trail/server/types.ts | 17 - .../server/audit/audit_logger.ts | 4 +- x-pack/plugins/security/README.md | 91 ++- .../server/audit/audit_events.test.ts | 204 +++++++ .../security/server/audit/audit_events.ts | 244 ++++++++ .../server/audit/audit_service.test.ts | 545 ++++++++++++++---- .../security/server/audit/audit_service.ts | 209 ++++++- .../security/server/audit/index.mock.ts | 3 + x-pack/plugins/security/server/audit/index.ts | 12 +- .../server/audit/security_audit_logger.ts | 16 +- .../authentication/authenticator.test.ts | 67 ++- .../server/authentication/authenticator.ts | 21 +- .../server/authentication/index.test.ts | 10 +- .../security/server/authentication/index.ts | 11 +- x-pack/plugins/security/server/config.test.ts | 228 ++++---- x-pack/plugins/security/server/config.ts | 29 +- x-pack/plugins/security/server/index.ts | 2 +- x-pack/plugins/security/server/plugin.test.ts | 1 + x-pack/plugins/security/server/plugin.ts | 41 +- .../security/server/saved_objects/index.ts | 11 +- ...ecure_saved_objects_client_wrapper.test.ts | 277 +++++++-- .../secure_saved_objects_client_wrapper.ts | 334 +++++++++-- .../plugins/spaces/server/lib/audit_logger.ts | 6 +- x-pack/scripts/functional_tests.js | 1 + x-pack/test/plugin_functional/config.ts | 7 - .../audit_trail_test/server/.gitignore | 1 - .../plugins/audit_trail_test/server/plugin.ts | 65 --- .../test_suites/audit_trail/index.ts | 129 ----- .../security_api_integration/audit.config.ts | 37 ++ .../fixtures/audit/audit_log}/kibana.json | 4 +- .../fixtures/audit/audit_log}/server/index.ts | 0 .../fixtures/audit/audit_log/server/plugin.ts | 20 + .../tests/audit/audit_log.ts | 118 ++++ .../tests/audit/index.ts | 14 + 93 files changed, 2452 insertions(+), 2025 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditableevent.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditor.add.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditor.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.auditorfactory.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.audittrailstart.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md delete mode 100644 src/core/server/audit_trail/audit_trail_service.mock.ts delete mode 100644 src/core/server/audit_trail/audit_trail_service.test.ts delete mode 100644 src/core/server/audit_trail/audit_trail_service.ts delete mode 100644 src/core/server/audit_trail/index.ts delete mode 100644 src/core/server/audit_trail/types.ts delete mode 100644 x-pack/plugins/audit_trail/kibana.json delete mode 100644 x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts delete mode 100644 x-pack/plugins/audit_trail/server/client/audit_trail_client.ts delete mode 100644 x-pack/plugins/audit_trail/server/config.test.ts delete mode 100644 x-pack/plugins/audit_trail/server/config.ts delete mode 100644 x-pack/plugins/audit_trail/server/index.ts delete mode 100644 x-pack/plugins/audit_trail/server/plugin.test.ts delete mode 100644 x-pack/plugins/audit_trail/server/plugin.ts delete mode 100644 x-pack/plugins/audit_trail/server/types.ts create mode 100644 x-pack/plugins/security/server/audit/audit_events.test.ts create mode 100644 x-pack/plugins/security/server/audit/audit_events.ts delete mode 100644 x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore delete mode 100644 x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts delete mode 100644 x-pack/test/plugin_functional/test_suites/audit_trail/index.ts create mode 100644 x-pack/test/security_api_integration/audit.config.ts rename x-pack/test/{plugin_functional/plugins/audit_trail_test => security_api_integration/fixtures/audit/audit_log}/kibana.json (62%) rename x-pack/test/{plugin_functional/plugins/audit_trail_test => security_api_integration/fixtures/audit/audit_log}/server/index.ts (100%) create mode 100644 x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts create mode 100644 x-pack/test/security_api_integration/tests/audit/audit_log.ts create mode 100644 x-pack/test/security_api_integration/tests/audit/index.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 3e849ca80db72..8e08c3806446d 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -286,10 +286,6 @@ which will load the visualization's editor. |To access an elasticsearch instance that has live data you have two options: -|{kib-repo}blob/{branch}/x-pack/plugins/audit_trail[auditTrail] -|WARNING: Missing README. - - |{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] |Notes: Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place @@ -469,7 +465,8 @@ Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/security/README.md[security] -|See Configuring security in Kibana. +|See Configuring security in +Kibana. |{kib-repo}blob/{branch}/x-pack/plugins/security_solution/README.md[securitySolution] diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.md deleted file mode 100644 index aa109c5064887..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditableevent.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) - -## AuditableEvent interface - -Event to audit. - -Signature: - -```typescript -export interface AuditableEvent -``` - -## Remarks - -Not a complete interface. - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [message](./kibana-plugin-core-server.auditableevent.message.md) | string | | -| [type](./kibana-plugin-core-server.auditableevent.type.md) | string | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md deleted file mode 100644 index 3ac4167c6998b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditableevent.message.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) > [message](./kibana-plugin-core-server.auditableevent.message.md) - -## AuditableEvent.message property - -Signature: - -```typescript -message: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md b/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md deleted file mode 100644 index 3748748366684..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditableevent.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) > [type](./kibana-plugin-core-server.auditableevent.type.md) - -## AuditableEvent.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.add.md b/docs/development/core/server/kibana-plugin-core-server.auditor.add.md deleted file mode 100644 index 40245a93753fc..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditor.add.md +++ /dev/null @@ -1,36 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) > [add](./kibana-plugin-core-server.auditor.add.md) - -## Auditor.add() method - -Add a record to audit log. Service attaches to a log record: - metadata about an end-user initiating an operation - scope name, if presents - -Signature: - -```typescript -add(event: AuditableEvent): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| event | AuditableEvent | | - -Returns: - -`void` - -## Example - -How to add a record in audit log: - -```typescript -router.get({ path: '/my_endpoint', validate: false }, async (context, request, response) => { - context.core.auditor.withAuditScope('my_plugin_operation'); - const value = await context.core.elasticsearch.legacy.client.callAsCurrentUser('...'); - context.core.add({ type: 'operation.type', message: 'perform an operation in ... endpoint' }); - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.md b/docs/development/core/server/kibana-plugin-core-server.auditor.md deleted file mode 100644 index 191a34df647ab..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditor.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) - -## Auditor interface - -Provides methods to log user actions and access events. - -Signature: - -```typescript -export interface Auditor -``` - -## Methods - -| Method | Description | -| --- | --- | -| [add(event)](./kibana-plugin-core-server.auditor.add.md) | Add a record to audit log. Service attaches to a log record: - metadata about an end-user initiating an operation - scope name, if presents | -| [withAuditScope(name)](./kibana-plugin-core-server.auditor.withauditscope.md) | Add a high-level scope name for logged events. It helps to identify the root cause of low-level events. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md b/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md deleted file mode 100644 index 0ae0c48ab92f4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditor.withauditscope.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [Auditor](./kibana-plugin-core-server.auditor.md) > [withAuditScope](./kibana-plugin-core-server.auditor.withauditscope.md) - -## Auditor.withAuditScope() method - -Add a high-level scope name for logged events. It helps to identify the root cause of low-level events. - -Signature: - -```typescript -withAuditScope(name: string): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| name | string | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md deleted file mode 100644 index 4a60931e60940..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.asscoped.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) > [asScoped](./kibana-plugin-core-server.auditorfactory.asscoped.md) - -## AuditorFactory.asScoped() method - -Signature: - -```typescript -asScoped(request: KibanaRequest): Auditor; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | KibanaRequest | | - -Returns: - -`Auditor` - diff --git a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md b/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md deleted file mode 100644 index fd4760caa3552..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.auditorfactory.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) - -## AuditorFactory interface - -Creates [Auditor](./kibana-plugin-core-server.auditor.md) instance bound to the current user credentials. - -Signature: - -```typescript -export interface AuditorFactory -``` - -## Methods - -| Method | Description | -| --- | --- | -| [asScoped(request)](./kibana-plugin-core-server.auditorfactory.asscoped.md) | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md deleted file mode 100644 index 50885232a088e..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) - -## AuditTrailSetup interface - -Signature: - -```typescript -export interface AuditTrailSetup -``` - -## Methods - -| Method | Description | -| --- | --- | -| [register(auditor)](./kibana-plugin-core-server.audittrailsetup.register.md) | Register a custom [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) implementation. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md b/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md deleted file mode 100644 index 36695844ced73..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.audittrailsetup.register.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) > [register](./kibana-plugin-core-server.audittrailsetup.register.md) - -## AuditTrailSetup.register() method - -Register a custom [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) implementation. - -Signature: - -```typescript -register(auditor: AuditorFactory): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| auditor | AuditorFactory | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md b/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md deleted file mode 100644 index 4fb9f5cb93549..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.audittrailstart.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md) - -## AuditTrailStart type - -Signature: - -```typescript -export declare type AuditTrailStart = AuditorFactory; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md deleted file mode 100644 index 1aa7a75b7a086..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.audittrail.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [auditTrail](./kibana-plugin-core-server.coresetup.audittrail.md) - -## CoreSetup.auditTrail property - -[AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) - -Signature: - -```typescript -auditTrail: AuditTrailSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 75da8df2ae15a..7a733cc34dace 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -16,7 +16,6 @@ export interface CoreSetupAuditTrailSetup | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | [capabilities](./kibana-plugin-core-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | | [context](./kibana-plugin-core-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | | [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md b/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md deleted file mode 100644 index 879e0df836190..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.audittrail.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) - -## CoreStart.auditTrail property - -[AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) - -Signature: - -```typescript -auditTrail: AuditTrailStart; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 0d5474fae5e16..f98088648689f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -16,7 +16,6 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [auditTrail](./kibana-plugin-core-server.corestart.audittrail.md) | AuditTrailStart | [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md index 6a56d31bbd55f..823f34bd7dd23 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyClusterClient` class Signature: ```typescript -constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFactory: () => AuditorFactory, getAuthHeaders?: GetAuthHeaders); +constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); ``` ## Parameters @@ -18,6 +18,5 @@ constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuditorFact | --- | --- | --- | | config | LegacyElasticsearchClientConfig | | | log | Logger | | -| getAuditorFactory | () => AuditorFactory | | | getAuthHeaders | GetAuthHeaders | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md index 668d0b2866a26..d24aeb44ca86a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyclusterclient.md @@ -21,7 +21,7 @@ export declare class LegacyClusterClient implements ILegacyClusterClient | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(config, log, getAuditorFactory, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | +| [(constructor)(config, log, getAuthHeaders)](./kibana-plugin-core-server.legacyclusterclient._constructor_.md) | | Constructs a new instance of the LegacyClusterClient class | ## Properties diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md index ffadab7656602..bd1cd1e9f3d9b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `LegacyScopedClusterClient` class Signature: ```typescript -constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); +constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); ``` ## Parameters @@ -19,5 +19,4 @@ constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller | internalAPICaller | LegacyAPICaller | | | scopedAPICaller | LegacyAPICaller | | | headers | Headers | undefined | | -| auditor | Auditor | undefined | | diff --git a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md index 7f752d70921ba..6b6649e833a92 100644 --- a/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.legacyscopedclusterclient.md @@ -21,7 +21,7 @@ export declare class LegacyScopedClusterClient implements ILegacyScopedClusterCl | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(internalAPICaller, scopedAPICaller, headers, auditor)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | +| [(constructor)(internalAPICaller, scopedAPICaller, headers)](./kibana-plugin-core-server.legacyscopedclusterclient._constructor_.md) | | Constructs a new instance of the LegacyScopedClusterClient class | ## Methods diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a484c856ec015..29f5220794918 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -53,10 +53,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppCategory](./kibana-plugin-core-server.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AssistanceAPIResponse](./kibana-plugin-core-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-core-server.assistantapiclientparams.md) | | -| [AuditableEvent](./kibana-plugin-core-server.auditableevent.md) | Event to audit. | -| [Auditor](./kibana-plugin-core-server.auditor.md) | Provides methods to log user actions and access events. | -| [AuditorFactory](./kibana-plugin-core-server.auditorfactory.md) | Creates [Auditor](./kibana-plugin-core-server.auditor.md) instance bound to the current user credentials. | -| [AuditTrailSetup](./kibana-plugin-core-server.audittrailsetup.md) | | | [Authenticated](./kibana-plugin-core-server.authenticated.md) | | | [AuthNotHandled](./kibana-plugin-core-server.authnothandled.md) | | | [AuthRedirected](./kibana-plugin-core-server.authredirected.md) | | @@ -132,7 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginConfigDescriptor](./kibana-plugin-core-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | -| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request | +| [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -223,7 +219,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | | [AppenderConfigType](./kibana-plugin-core-server.appenderconfigtype.md) | | -| [AuditTrailStart](./kibana-plugin-core-server.audittrailstart.md) | | | [AuthenticationHandler](./kibana-plugin-core-server.authenticationhandler.md) | See [AuthToolkit](./kibana-plugin-core-server.authtoolkit.md). | | [AuthHeaders](./kibana-plugin-core-server.authheaders.md) | Auth Headers map | | [AuthResult](./kibana-plugin-core-server.authresult.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md index 5b8492ec5ece1..b195e97989162 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.core.md @@ -21,6 +21,5 @@ core: { uiSettings: { client: IUiSettingsClient; }; - auditor: Auditor; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md index 4e530973f9d50..1de7313f2c40e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request - [uiSettings.auditor](./kibana-plugin-core-server.auditor.md) - AuditTrail client scoped to the incoming request +Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
auditor: Auditor;
} | | +| [core](./kibana-plugin-core-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
};
elasticsearch: {
client: IScopedClusterClient;
legacy: {
client: ILegacyScopedClusterClient;
};
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index c743aa43fab05..6b01094f7248a 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -155,7 +155,7 @@ There is a very limited set of cases when you'd want to change these settings. F | `xpack.security.authc.http.autoSchemesEnabled` | Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. -| `xpack.security.authc.http.schemes` +| `xpack.security.authc.http.schemes[]` | List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <> scheme. |=== @@ -240,7 +240,6 @@ The format is a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', |=== -[float] [[security-encrypted-saved-objects-settings]] ==== Encrypted saved objects settings @@ -261,4 +260,112 @@ In high-availability deployments, make sure you use the same encryption and decr `keyRotation.decryptionOnlyKeys` | An optional list of previously used encryption keys. Like <>, these must be at least 32 characters in length. {kib} doesn't use these keys for encryption, but may still require them to decrypt some existing saved objects. Use this setting if you wish to change your encryption key, but don't want to lose access to saved objects that were previously encrypted with a different key. -|=== \ No newline at end of file +|=== + +[float] +[[audit-logging-settings]] +===== Audit logging settings + +You can enable audit logging to support compliance, accountability, and security. When enabled, {kib} will capture: + +- Who performed an action +- What action was performed +- When the action occurred + +For more details and a reference of audit events, refer to <>. + +[cols="2*<"] +|=== +| `xpack.security.audit.enabled` +| Set to `true` to enable audit logging for security events. *Default:* `false` +|=== + +[float] +[[ecs-audit-logging-settings]] +===== ECS audit logging settings + +To enable the <>, specify where you want to write the audit events using `xpack.security.audit.appender`. + +[cols="2*<"] +|=== +| `xpack.security.audit.appender` +| Optional. Specifies where audit logs should be written to and how they should be formatted. + +2+a| For example: + +[source,yaml] +---------------------------------------- +xpack.security.audit.appender: + kind: file + path: /path/to/audit.log + layout: + kind: json +---------------------------------------- + +| `xpack.security.audit.appender.kind` +| Required. Specifies where audit logs should be written to. Allowed values are `console` or `file`. +|=== + +[float] +[[audit-logging-file-appender]] +===== File appender + +The file appender can be configured using the following settings: + +[cols="2*<"] +|=== +| `xpack.security.audit.appender.path` +| Required. Full file path the log file should be written to. + +| `xpack.security.audit.appender.layout.kind` +| Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`. +|=== + +[float] +[[audit-logging-pattern-layout]] +===== Pattern layout + +The pattern layout can be configured using the following settings: + +[cols="2*<"] +|=== +| `xpack.security.audit.appender.layout.highlight` +| Optional. Set to `true` to enable highlighting log messages with colors. + +| `xpack.security.audit.appender.layout.pattern` +| Optional. Specifies how the log line should be formatted. *Default:* `[%date][%level][%logger]%meta %message` +|=== + +[float] +[[audit-logging-ignore-filters]] +===== Ignore filters + +[cols="2*<"] +|=== +| `xpack.security.audit.ignore_filters[]` +| List of filters that determine which events should be excluded from the audit log. An event will get filtered out if at least one of the provided filters matches. + +2+a| For example: + +[source,yaml] +---------------------------------------- +xpack.security.audit.ignore_filters: +- actions: [http_request] <1> +- categories: [database] + types: [creation, change, deletion] <2> +---------------------------------------- +<1> Filters out HTTP request events +<2> Filters out any data write events + +| `xpack.security.audit.ignore_filters[].actions[]` +| List of values matched against the `event.action` field of an audit event. Refer to <> for a list of available events. + +| `xpack.security.audit.ignore_filters[].categories[]` +| List of values matched against the `event.category` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-category.html[ECS categorization field] for allowed values. + +| `xpack.security.audit.ignore_filters[].types[]` +| List of values matched against the `event.type` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-type.html[ECS type field] for allowed values. + +| `xpack.security.audit.ignore_filters[].outcomes[]` +| List of values matched against the `event.outcome` field of an audit event. Refer to https://www.elastic.co/guide/en/ecs/1.5/ecs-allowed-values-event-outcome.html[ECS outcome field] for allowed values. +|=== diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index a7359af38c1cb..d4370c4d840c0 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -3,30 +3,30 @@ === Audit logs You can enable auditing to keep track of security-related events such as -authorization success and failures. Logging these events enables you -to monitor {kib} for suspicious activity and provides evidence in the -event of an attack. +authorization success and failures. Logging these events enables you to monitor +{kib} for suspicious activity and provides evidence in the event of an attack. -Use the {kib} audit logs in conjunction with {es}'s -audit logging to get a holistic view of all security related events. -{kib} defers to {es}'s security model for authentication, data -index authorization, and features that are driven by cluster-wide privileges. -For more information on enabling audit logging in {es}, see -{ref}/auditing.html[Auditing security events]. +Use the {kib} audit logs in conjunction with {ref}/enable-audit-logging.html[{es} audit logging] to get a +holistic view of all security related events. {kib} defers to the {es} security +model for authentication, data index authorization, and features that are driven +by cluster-wide privileges. For more information on enabling audit logging in +{es}, refer to {ref}/auditing.html[Auditing security events]. [IMPORTANT] ============================================================================ -Audit logs are **disabled** by default. To enable this functionality, you -must set `xpack.security.audit.enabled` to `true` in `kibana.yml`. +Audit logs are **disabled** by default. To enable this functionality, you must +set `xpack.security.audit.enabled` to `true` in `kibana.yml`. ============================================================================ -Audit logging uses the standard {kib} logging output, which can be configured -in the `kibana.yml` and is discussed in <>. +The current version of the audit logger uses the standard {kib} logging output, +which can be configured in `kibana.yml`. For more information, refer to <>. +The audit logger uses a separate logger and can be configured using +the options in <>. ==== Audit event types -When you are auditing security events, each request can generate -multiple audit events. The following is a list of the events that can be generated: +When you are auditing security events, each request can generate multiple audit +events. The following is a list of the events that can be generated: |====== | `saved_objects_authorization_success` | Logged when a user is authorized to access a saved @@ -34,3 +34,110 @@ multiple audit events. The following is a list of the events that can be generat | `saved_objects_authorization_failure` | Logged when a user isn't authorized to access a saved objects when using a role with <> |====== + +[[xpack-security-ecs-audit-logging]] +==== ECS audit events + +[IMPORTANT] +============================================================================ +The following events are only logged if the ECS audit logger is enabled. +For information on how to configure `xpack.security.audit.appender`, refer to +<>. +============================================================================ + +Refer to the table of events that can be logged for auditing purposes. + +Each event is broken down into `category`, `type`, `action` and `outcome` fields +to make it easy to filter, query and aggregate the resulting logs. + +[NOTE] +============================================================================ +To ensure that a record of every operation is persisted even in case of an +unexpected error, asynchronous write operations are logged immediately after all +authorization checks have passed, but before the response from {es} is received. +Refer to the corresponding {es} logs for potential write errors. +============================================================================ + + +[cols="3*<"] +|====== +3+a| +===== Category: authentication + +| *Action* +| *Outcome* +| *Description* + +.2+| `user_login` +| `success` | User has logged in successfully. +| `failure` | Failed login attempt (e.g. due to invalid credentials). + +3+a| +===== Category: database +====== Type: creation + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_create` +| `unknown` | User is creating a saved object. +| `failure` | User is not authorized to create a saved object. + + +3+a| +====== Type: change + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_update` +| `unknown` | User is updating a saved object. +| `failure` | User is not authorized to update a saved object. + +.2+| `saved_object_add_to_spaces` +| `unknown` | User is adding a saved object to other spaces. +| `failure` | User is not authorized to add a saved object to other spaces. + +.2+| `saved_object_delete_from_spaces` +| `unknown` | User is removing a saved object from other spaces. +| `failure` | User is not authorized to remove a saved object from other spaces. + +3+a| +====== Type: deletion + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_delete` +| `unknown` | User is deleting a saved object. +| `failure` | User is not authorized to delete a saved object. + +3+a| +====== Type: access + +| *Action* +| *Outcome* +| *Description* + +.2+| `saved_object_get` +| `success` | User has accessed a saved object. +| `failure` | User is not authorized to access a saved object. + +.2+| `saved_object_find` +| `success` | User has accessed a saved object as part of a search operation. +| `failure` | User is not authorized to search for saved objects. + + +3+a| +===== Category: web + +| *Action* +| *Outcome* +| *Description* + +| `http_request` +| `unknown` | User is making an HTTP request. +|====== diff --git a/src/core/server/audit_trail/audit_trail_service.mock.ts b/src/core/server/audit_trail/audit_trail_service.mock.ts deleted file mode 100644 index 4c9c064840750..0000000000000 --- a/src/core/server/audit_trail/audit_trail_service.mock.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { AuditTrailSetup, AuditTrailStart, Auditor } from './types'; -import { AuditTrailService } from './audit_trail_service'; - -const createSetupContractMock = () => { - const mocked: jest.Mocked = { - register: jest.fn(), - }; - return mocked; -}; - -const createAuditorMock = () => { - const mocked: jest.Mocked = { - add: jest.fn(), - withAuditScope: jest.fn(), - }; - return mocked; -}; - -const createStartContractMock = () => { - const mocked: jest.Mocked = { - asScoped: jest.fn(), - }; - mocked.asScoped.mockReturnValue(createAuditorMock()); - return mocked; -}; - -const createServiceMock = (): jest.Mocked> => ({ - setup: jest.fn().mockResolvedValue(createSetupContractMock()), - start: jest.fn().mockResolvedValue(createStartContractMock()), - stop: jest.fn(), -}); - -export const auditTrailServiceMock = { - create: createServiceMock, - createSetupContract: createSetupContractMock, - createStartContract: createStartContractMock, - createAuditorFactory: createStartContractMock, - createAuditor: createAuditorMock, -}; diff --git a/src/core/server/audit_trail/audit_trail_service.test.ts b/src/core/server/audit_trail/audit_trail_service.test.ts deleted file mode 100644 index 63b45b62275b6..0000000000000 --- a/src/core/server/audit_trail/audit_trail_service.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AuditTrailService } from './audit_trail_service'; -import { AuditorFactory } from './types'; -import { mockCoreContext } from '../core_context.mock'; -import { httpServerMock } from '../http/http_server.mocks'; - -describe('AuditTrailService', () => { - const coreContext = mockCoreContext.create(); - - describe('#setup', () => { - describe('register', () => { - it('throws if registered the same auditor factory twice', () => { - const auditTrail = new AuditTrailService(coreContext); - const { register } = auditTrail.setup(); - const auditorFactory: AuditorFactory = { - asScoped() { - return { add: () => undefined, withAuditScope: (() => {}) as any }; - }, - }; - register(auditorFactory); - expect(() => register(auditorFactory)).toThrowErrorMatchingInlineSnapshot( - `"An auditor factory has been already registered"` - ); - }); - }); - }); - - describe('#start', () => { - describe('asScoped', () => { - it('initialize every auditor with a request', () => { - const scopedMock = jest.fn(() => ({ add: jest.fn(), withAuditScope: jest.fn() })); - const auditorFactory = { asScoped: scopedMock }; - - const auditTrail = new AuditTrailService(coreContext); - const { register } = auditTrail.setup(); - register(auditorFactory); - - const { asScoped } = auditTrail.start(); - const kibanaRequest = httpServerMock.createKibanaRequest(); - asScoped(kibanaRequest); - - expect(scopedMock).toHaveBeenCalledWith(kibanaRequest); - }); - - it('passes auditable event to an auditor', () => { - const addEventMock = jest.fn(); - const auditorFactory = { - asScoped() { - return { add: addEventMock, withAuditScope: jest.fn() }; - }, - }; - - const auditTrail = new AuditTrailService(coreContext); - const { register } = auditTrail.setup(); - register(auditorFactory); - - const { asScoped } = auditTrail.start(); - const kibanaRequest = httpServerMock.createKibanaRequest(); - const auditor = asScoped(kibanaRequest); - const message = { - type: 'foo', - message: 'bar', - }; - auditor.add(message); - - expect(addEventMock).toHaveBeenLastCalledWith(message); - }); - - describe('return the same auditor instance for the same KibanaRequest', () => { - const auditTrail = new AuditTrailService(coreContext); - auditTrail.setup(); - const { asScoped } = auditTrail.start(); - - const rawRequest1 = httpServerMock.createKibanaRequest(); - const rawRequest2 = httpServerMock.createKibanaRequest(); - expect(asScoped(rawRequest1)).toBe(asScoped(rawRequest1)); - expect(asScoped(rawRequest1)).not.toBe(asScoped(rawRequest2)); - }); - }); - }); -}); diff --git a/src/core/server/audit_trail/audit_trail_service.ts b/src/core/server/audit_trail/audit_trail_service.ts deleted file mode 100644 index f1841858dbc92..0000000000000 --- a/src/core/server/audit_trail/audit_trail_service.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { CoreService } from '../../types'; -import { CoreContext } from '../core_context'; -import { Logger } from '../logging'; -import { KibanaRequest, LegacyRequest } from '../http'; -import { ensureRawRequest } from '../http/router'; -import { Auditor, AuditorFactory, AuditTrailSetup, AuditTrailStart } from './types'; - -const defaultAuditorFactory: AuditorFactory = { - asScoped() { - return { - add() {}, - withAuditScope() {}, - }; - }, -}; - -export class AuditTrailService implements CoreService { - private readonly log: Logger; - private auditor: AuditorFactory = defaultAuditorFactory; - private readonly auditors = new WeakMap(); - - constructor(core: CoreContext) { - this.log = core.logger.get('audit_trail'); - } - - setup() { - return { - register: (auditor: AuditorFactory) => { - if (this.auditor !== defaultAuditorFactory) { - throw new Error('An auditor factory has been already registered'); - } - this.auditor = auditor; - this.log.debug('An auditor factory has been registered'); - }, - }; - } - - start() { - return { - asScoped: (request: KibanaRequest) => { - const key = ensureRawRequest(request); - if (!this.auditors.has(key)) { - this.auditors.set(key, this.auditor!.asScoped(request)); - } - return this.auditors.get(key)!; - }, - }; - } - - stop() {} -} diff --git a/src/core/server/audit_trail/index.ts b/src/core/server/audit_trail/index.ts deleted file mode 100644 index 3f01e6fa3582d..0000000000000 --- a/src/core/server/audit_trail/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { AuditTrailService } from './audit_trail_service'; -export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup, AuditTrailStart } from './types'; diff --git a/src/core/server/audit_trail/types.ts b/src/core/server/audit_trail/types.ts deleted file mode 100644 index b3c1fc3c222fa..0000000000000 --- a/src/core/server/audit_trail/types.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { KibanaRequest } from '../http'; - -/** - * Event to audit. - * @public - * - * @remarks - * Not a complete interface. - */ -export interface AuditableEvent { - message: string; - type: string; -} - -/** - * Provides methods to log user actions and access events. - * @public - */ -export interface Auditor { - /** - * Add a record to audit log. - * Service attaches to a log record: - * - metadata about an end-user initiating an operation - * - scope name, if presents - * - * @example - * How to add a record in audit log: - * ```typescript - * router.get({ path: '/my_endpoint', validate: false }, async (context, request, response) => { - * context.core.auditor.withAuditScope('my_plugin_operation'); - * const value = await context.core.elasticsearch.legacy.client.callAsCurrentUser('...'); - * context.core.add({ type: 'operation.type', message: 'perform an operation in ... endpoint' }); - * ``` - */ - add(event: AuditableEvent): void; - /** - * Add a high-level scope name for logged events. - * It helps to identify the root cause of low-level events. - */ - withAuditScope(name: string): void; -} - -/** - * Creates {@link Auditor} instance bound to the current user credentials. - * @public - */ -export interface AuditorFactory { - asScoped(request: KibanaRequest): Auditor; -} - -export interface AuditTrailSetup { - /** - * Register a custom {@link AuditorFactory} implementation. - */ - register(auditor: AuditorFactory): void; -} - -export type AuditTrailStart = AuditorFactory; diff --git a/src/core/server/core_route_handler_context.test.ts b/src/core/server/core_route_handler_context.test.ts index 563e337e6c7e0..d4599d91c1b96 100644 --- a/src/core/server/core_route_handler_context.test.ts +++ b/src/core/server/core_route_handler_context.test.ts @@ -19,41 +19,6 @@ import { CoreRouteHandlerContext } from './core_route_handler_context'; import { coreMock, httpServerMock } from './mocks'; -describe('#auditor', () => { - test('returns the results of coreStart.audiTrail.asScoped', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - const auditor = context.auditor; - expect(auditor).toBe(coreStart.auditTrail.asScoped.mock.results[0].value); - }); - - test('lazily created', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - expect(coreStart.auditTrail.asScoped).not.toHaveBeenCalled(); - const auditor = context.auditor; - expect(coreStart.auditTrail.asScoped).toHaveBeenCalled(); - expect(auditor).toBeDefined(); - }); - - test('only creates one instance', () => { - const request = httpServerMock.createKibanaRequest(); - const coreStart = coreMock.createInternalStart(); - const context = new CoreRouteHandlerContext(coreStart, request); - - const auditor1 = context.auditor; - const auditor2 = context.auditor; - expect(coreStart.auditTrail.asScoped.mock.calls.length).toBe(1); - const mockResult = coreStart.auditTrail.asScoped.mock.results[0].value; - expect(auditor1).toBe(mockResult); - expect(auditor2).toBe(mockResult); - }); -}); - describe('#elasticsearch', () => { describe('#client', () => { test('returns the results of coreStart.elasticsearch.client.asScoped', () => { diff --git a/src/core/server/core_route_handler_context.ts b/src/core/server/core_route_handler_context.ts index 8a182a523f52c..520c5bd3f685b 100644 --- a/src/core/server/core_route_handler_context.ts +++ b/src/core/server/core_route_handler_context.ts @@ -27,7 +27,6 @@ import { IScopedClusterClient, LegacyScopedClusterClient, } from './elasticsearch'; -import { Auditor } from './audit_trail'; import { InternalUiSettingsServiceStart, IUiSettingsClient } from './ui_settings'; class CoreElasticsearchRouteHandlerContext { @@ -99,8 +98,6 @@ class CoreUiSettingsRouteHandlerContext { } export class CoreRouteHandlerContext { - #auditor?: Auditor; - readonly elasticsearch: CoreElasticsearchRouteHandlerContext; readonly savedObjects: CoreSavedObjectsRouteHandlerContext; readonly uiSettings: CoreUiSettingsRouteHandlerContext; @@ -122,11 +119,4 @@ export class CoreRouteHandlerContext { this.savedObjects ); } - - public get auditor() { - if (this.#auditor == null) { - this.#auditor = this.coreStart.auditTrail.asScoped(this.request); - } - return this.#auditor; - } } diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index ce82410f6061e..e527fdb915970 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -26,7 +26,6 @@ import { configServiceMock, getEnvOptions } from '../config/mocks'; import { CoreContext } from '../core_context'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { httpServiceMock } from '../http/http_service.mock'; -import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; @@ -41,9 +40,6 @@ const configService = configServiceMock.create(); const setupDeps = { http: httpServiceMock.createInternalSetupContract(), }; -const startDeps = { - auditTrail: auditTrailServiceMock.createStartContract(), -}; configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], @@ -113,7 +109,6 @@ describe('#setup', () => { expect(MockLegacyClusterClient).toHaveBeenCalledWith( expect.objectContaining(customConfig), expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }), - expect.any(Function), expect.any(Function) ); }); @@ -260,14 +255,14 @@ describe('#setup', () => { describe('#start', () => { it('throws if called before `setup`', async () => { - expect(() => elasticsearchService.start(startDeps)).rejects.toMatchInlineSnapshot( + expect(() => elasticsearchService.start()).rejects.toMatchInlineSnapshot( `[Error: ElasticsearchService needs to be setup before calling start]` ); }); it('returns elasticsearch client as a part of the contract', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); const client = startContract.client; expect(client.asInternalUser).toBe(mockClusterClientInstance.asInternalUser); @@ -276,7 +271,7 @@ describe('#start', () => { describe('#createClient', () => { it('allows to specify config properties', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); // reset all mocks called during setup phase MockClusterClient.mockClear(); @@ -295,7 +290,7 @@ describe('#start', () => { }); it('creates a new client on each call', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); // reset all mocks called during setup phase MockClusterClient.mockClear(); @@ -310,7 +305,7 @@ describe('#start', () => { it('falls back to elasticsearch default config values if property not specified', async () => { await elasticsearchService.setup(setupDeps); - const startContract = await elasticsearchService.start(startDeps); + const startContract = await elasticsearchService.start(); // reset all mocks called during setup phase MockClusterClient.mockClear(); @@ -347,7 +342,7 @@ describe('#start', () => { describe('#stop', () => { it('stops both legacy and new clients', async () => { await elasticsearchService.setup(setupDeps); - await elasticsearchService.start(startDeps); + await elasticsearchService.start(); await elasticsearchService.stop(); expect(mockLegacyClusterClientInstance.close).toHaveBeenCalledTimes(1); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 5d07840e8bda7..a0b9e8c6f2bfb 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -32,7 +32,6 @@ import { import { ClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; -import { AuditTrailStart, AuditorFactory } from '../audit_trail'; import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart } from './types'; import { pollEsNodesVersion } from './version_check/ensure_es_version'; import { calculateStatus$ } from './status'; @@ -41,16 +40,11 @@ interface SetupDeps { http: InternalHttpServiceSetup; } -interface StartDeps { - auditTrail: AuditTrailStart; -} - /** @internal */ export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private auditorFactory?: AuditorFactory; private stop$ = new Subject(); private kibanaVersion: string; private getAuthHeaders?: GetAuthHeaders; @@ -103,8 +97,7 @@ export class ElasticsearchService status$: calculateStatus$(esNodesCompatibility$), }; } - public async start({ auditTrail }: StartDeps): Promise { - this.auditorFactory = auditTrail; + public async start(): Promise { if (!this.legacyClient || !this.createLegacyCustomClient) { throw new Error('ElasticsearchService needs to be setup before calling start'); } @@ -153,15 +146,7 @@ export class ElasticsearchService return new LegacyClusterClient( config, this.coreContext.logger.get('elasticsearch', type), - this.getAuditorFactory, this.getAuthHeaders ); } - - private getAuditorFactory = () => { - if (!this.auditorFactory) { - throw new Error('auditTrail has not been initialized'); - } - return this.auditorFactory; - }; } diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 745ef4304d0b1..812f81a1affd6 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -27,7 +27,6 @@ import { import { errors } from 'elasticsearch'; import { get } from 'lodash'; -import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock'; import { Logger } from '../../logging'; import { loggingSystemMock } from '../../logging/logging_system.mock'; import { httpServerMock } from '../../http/http_server.mocks'; @@ -43,11 +42,7 @@ test('#constructor creates client with parsed config', () => { const mockEsConfig = { apiVersion: 'es-version' } as any; const mockLogger = logger.get(); - const clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + const clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); expect(clusterClient).toBeDefined(); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); @@ -73,11 +68,7 @@ describe('#callAsInternalUser', () => { }; MockClient.mockImplementation(() => mockEsClientInstance); - clusterClient = new LegacyClusterClient( - { apiVersion: 'es-version' } as any, - logger.get(), - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient({ apiVersion: 'es-version' } as any, logger.get()); }); test('fails if cluster client is closed', async () => { @@ -246,11 +237,7 @@ describe('#asScoped', () => { requestHeadersWhitelist: ['one', 'two'], } as any; - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); jest.clearAllMocks(); }); @@ -285,11 +272,7 @@ describe('#asScoped', () => { test('properly configures `ignoreCertAndKey` for various configurations', () => { // Config without SSL. - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -302,11 +285,7 @@ describe('#asScoped', () => { // Config ssl.alwaysPresentCertificate === false mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: false } } as any; - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -319,11 +298,7 @@ describe('#asScoped', () => { // Config ssl.alwaysPresentCertificate === true mockEsConfig = { ...mockEsConfig, ssl: { alwaysPresentCertificate: true } } as any; - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); @@ -344,8 +319,7 @@ describe('#asScoped', () => { expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' }, - expect.any(Object) + { one: '1', two: '2' } ); }); @@ -360,8 +334,7 @@ describe('#asScoped', () => { expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { 'x-opaque-id': 'alpha' }, - expect.any(Object) + { 'x-opaque-id': 'alpha' } ); }); @@ -383,142 +356,75 @@ describe('#asScoped', () => { }); test('does not fail when scope to not defined request', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); clusterClient.asScoped(); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {}, - undefined + {} ); }); test('does not fail when scope to a request without headers', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); clusterClient.asScoped({} as any); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - {}, - undefined + {} ); }); test('calls getAuthHeaders and filters results for a real request', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory, - () => ({ - one: '1', - three: '3', - }) - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ + one: '1', + three: '3', + })); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' }, - expect.any(Object) + { one: '1', two: '2' } ); }); test('getAuthHeaders results rewrite extends a request headers', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory, - () => ({ one: 'foo' }) - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({ one: 'foo' })); clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } })); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: 'foo', two: '2' }, - expect.any(Object) + { one: 'foo', two: '2' } ); }); test("doesn't call getAuthHeaders for a fake request", async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory, - () => ({}) - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => ({})); clusterClient.asScoped({ headers: { one: 'foo' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: 'foo' }, - undefined + { one: 'foo' } ); }); test('filters a fake request headers', async () => { - clusterClient = new LegacyClusterClient( - mockEsConfig, - mockLogger, - auditTrailServiceMock.createAuditorFactory - ); + clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger); clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( expect.any(Function), expect.any(Function), - { one: '1', two: '2' }, - undefined + { one: '1', two: '2' } ); }); - - describe('Auditor', () => { - it('creates Auditor for KibanaRequest', async () => { - const auditor = auditTrailServiceMock.createAuditor(); - const auditorFactory = auditTrailServiceMock.createAuditorFactory(); - auditorFactory.asScoped.mockReturnValue(auditor); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, () => auditorFactory); - clusterClient.asScoped(httpServerMock.createKibanaRequest()); - - expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); - expect(MockScopedClusterClient).toHaveBeenCalledWith( - expect.any(Function), - expect.any(Function), - expect.objectContaining({ 'x-opaque-id': expect.any(String) }), - auditor - ); - }); - - it("doesn't create Auditor for a fake request", async () => { - const getAuthHeaders = jest.fn(); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); - clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); - - expect(getAuthHeaders).not.toHaveBeenCalled(); - }); - - it("doesn't create Auditor when no request passed", async () => { - const getAuthHeaders = jest.fn(); - clusterClient = new LegacyClusterClient(mockEsConfig, mockLogger, getAuthHeaders); - clusterClient.asScoped(); - - expect(getAuthHeaders).not.toHaveBeenCalled(); - }); - }); }); describe('#close', () => { @@ -536,8 +442,7 @@ describe('#close', () => { clusterClient = new LegacyClusterClient( { apiVersion: 'es-version', requestHeadersWhitelist: [] } as any, - logger.get(), - auditTrailServiceMock.createAuditorFactory + logger.get() ); }); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.ts b/src/core/server/elasticsearch/legacy/cluster_client.ts index 81cbb5a10d7c6..00417e3bef4f4 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.ts @@ -20,8 +20,7 @@ import { Client } from 'elasticsearch'; import { get } from 'lodash'; import { LegacyElasticsearchErrorHelpers } from './errors'; -import { GetAuthHeaders, KibanaRequest, isKibanaRequest, isRealRequest } from '../../http'; -import { AuditorFactory } from '../../audit_trail'; +import { GetAuthHeaders, isKibanaRequest, isRealRequest } from '../../http'; import { filterHeaders, ensureRawRequest } from '../../http/router'; import { Logger } from '../../logging'; import { ScopeableRequest } from '../types'; @@ -132,7 +131,6 @@ export class LegacyClusterClient implements ILegacyClusterClient { constructor( private readonly config: LegacyElasticsearchClientConfig, private readonly log: Logger, - private readonly getAuditorFactory: () => AuditorFactory, private readonly getAuthHeaders: GetAuthHeaders = noop ) { this.client = new Client(parseElasticsearchClientConfig(config, log)); @@ -210,20 +208,10 @@ export class LegacyClusterClient implements ILegacyClusterClient { filterHeaders(this.getHeaders(request), [ 'x-opaque-id', ...this.config.requestHeadersWhitelist, - ]), - this.getScopedAuditor(request) + ]) ); } - private getScopedAuditor(request?: ScopeableRequest) { - // TODO: support alternative credential owners from outside of Request context in #39430 - if (request && isRealRequest(request)) { - const kibanaRequest = isKibanaRequest(request) ? request : KibanaRequest.from(request); - const auditorFactory = this.getAuditorFactory(); - return auditorFactory.asScoped(kibanaRequest); - } - } - /** * Calls specified endpoint with provided clientParams on behalf of the * user initiated request to the Kibana server (via HTTP request headers). diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts index f1096d5d602f4..2eb8cefb564ae 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.test.ts @@ -18,7 +18,6 @@ */ import { LegacyScopedClusterClient } from './scoped_cluster_client'; -import { auditTrailServiceMock } from '../../audit_trail/audit_trail_service.mock'; let internalAPICaller: jest.Mock; let scopedAPICaller: jest.Mock; @@ -84,28 +83,6 @@ describe('#callAsInternalUser', () => { expect(scopedAPICaller).not.toHaveBeenCalled(); }); - - describe('Auditor', () => { - it('does not fail when no auditor provided', () => { - const clusterClientWithoutAuditor = new LegacyScopedClusterClient(jest.fn(), jest.fn()); - expect(() => clusterClientWithoutAuditor.callAsInternalUser('endpoint')).not.toThrow(); - }); - it('creates an audit record if auditor provided', () => { - const auditor = auditTrailServiceMock.createAuditor(); - const clusterClientWithoutAuditor = new LegacyScopedClusterClient( - jest.fn(), - jest.fn(), - {}, - auditor - ); - clusterClientWithoutAuditor.callAsInternalUser('endpoint'); - expect(auditor.add).toHaveBeenCalledTimes(1); - expect(auditor.add).toHaveBeenLastCalledWith({ - message: 'endpoint', - type: 'elasticsearch.call.internalUser', - }); - }); - }); }); describe('#callAsCurrentUser', () => { @@ -229,26 +206,4 @@ describe('#callAsCurrentUser', () => { expect(internalAPICaller).not.toHaveBeenCalled(); }); - - describe('Auditor', () => { - it('does not fail when no auditor provided', () => { - const clusterClientWithoutAuditor = new LegacyScopedClusterClient(jest.fn(), jest.fn()); - expect(() => clusterClientWithoutAuditor.callAsCurrentUser('endpoint')).not.toThrow(); - }); - it('creates an audit record if auditor provided', () => { - const auditor = auditTrailServiceMock.createAuditor(); - const clusterClientWithoutAuditor = new LegacyScopedClusterClient( - jest.fn(), - jest.fn(), - {}, - auditor - ); - clusterClientWithoutAuditor.callAsCurrentUser('endpoint'); - expect(auditor.add).toHaveBeenCalledTimes(1); - expect(auditor.add).toHaveBeenLastCalledWith({ - message: 'endpoint', - type: 'elasticsearch.call.currentUser', - }); - }); - }); }); diff --git a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts index aee7a1daa8166..65484f0927c9e 100644 --- a/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts +++ b/src/core/server/elasticsearch/legacy/scoped_cluster_client.ts @@ -18,7 +18,6 @@ */ import { intersection, isObject } from 'lodash'; -import { Auditor } from '../../audit_trail'; import { Headers } from '../../http/router'; import { LegacyAPICaller, LegacyCallAPIOptions } from './api_types'; @@ -47,8 +46,7 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { constructor( private readonly internalAPICaller: LegacyAPICaller, private readonly scopedAPICaller: LegacyAPICaller, - private readonly headers?: Headers, - private readonly auditor?: Auditor + private readonly headers?: Headers ) { this.callAsCurrentUser = this.callAsCurrentUser.bind(this); this.callAsInternalUser = this.callAsInternalUser.bind(this); @@ -68,13 +66,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { clientParams: Record = {}, options?: LegacyCallAPIOptions ) { - if (this.auditor) { - this.auditor.add({ - message: endpoint, - type: 'elasticsearch.call.internalUser', - }); - } - return this.internalAPICaller(endpoint, clientParams, options); } @@ -107,13 +98,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { clientParams.headers = Object.assign({}, clientParams.headers, this.headers); } - if (this.auditor) { - this.auditor.add({ - message: endpoint, - type: 'elasticsearch.call.currentUser', - }); - } - return this.scopedAPICaller(endpoint, clientParams, options); } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index fc091bd17bdf4..efb196590ea97 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -62,7 +62,6 @@ import { import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; -import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; @@ -77,7 +76,6 @@ import { export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData }; -export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup } from './audit_trail'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; export { @@ -378,7 +376,6 @@ export { CoreUsageDataStart } from './core_usage_data'; * data client which uses the credentials of the incoming request * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client * which uses the credentials of the incoming request - * - {@link Auditor | uiSettings.auditor} - AuditTrail client scoped to the incoming request * * @public */ @@ -397,7 +394,6 @@ export interface RequestHandlerContext { uiSettings: { client: IUiSettingsClient; }; - auditor: Auditor; }; } @@ -434,8 +430,6 @@ export interface CoreSetup; - /** {@link AuditTrailSetup} */ - auditTrail: AuditTrailSetup; } /** @@ -469,8 +463,6 @@ export interface CoreStart { savedObjects: SavedObjectsServiceStart; /** {@link UiSettingsServiceStart} */ uiSettings: UiSettingsServiceStart; - /** {@link AuditTrailSetup} */ - auditTrail: AuditTrailStart; /** @internal {@link CoreUsageDataStart} */ coreUsageData: CoreUsageDataStart; } @@ -483,7 +475,6 @@ export { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId, - AuditTrailStart, }; /** diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index ce58348a14153..294af5ec34c3e 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -37,7 +37,6 @@ import { InternalMetricsServiceSetup, InternalMetricsServiceStart } from './metr import { InternalRenderingServiceSetup } from './rendering'; import { InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; -import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; @@ -53,7 +52,6 @@ export interface InternalCoreSetup { environment: InternalEnvironmentServiceSetup; rendering: InternalRenderingServiceSetup; httpResources: InternalHttpResourcesSetup; - auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; } @@ -68,7 +66,6 @@ export interface InternalCoreStart { metrics: InternalMetricsServiceStart; savedObjects: InternalSavedObjectsServiceStart; uiSettings: InternalUiSettingsServiceStart; - auditTrail: AuditTrailStart; coreUsageData: CoreUsageDataStart; } diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 57009f0d35c16..b8f5757f0b67f 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -44,7 +44,6 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; import { statusServiceMock } from '../status/status_service.mock'; -import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; @@ -92,7 +91,6 @@ beforeEach(() => { rendering: renderingServiceMock, environment: environmentSetup, status: statusServiceMock.createInternalSetupContract(), - auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 75e8ae6524920..c42771179aba2 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -216,7 +216,6 @@ export class LegacyService implements CoreService { getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, - auditTrail: startDeps.core.auditTrail, coreUsageData: { getCoreUsageData: () => { throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy'); @@ -284,7 +283,6 @@ export class LegacyService implements CoreService { uiSettings: { register: setupDeps.core.uiSettings.register, }, - auditTrail: setupDeps.core.auditTrail, getStartServices: () => Promise.resolve([coreStart, startDeps.plugins, {}]), }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 34e85920efb24..e47d06409894e 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -36,7 +36,6 @@ import { capabilitiesServiceMock } from './capabilities/capabilities_service.moc import { metricsServiceMock } from './metrics/metrics_service.mock'; import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; -import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; export { configServiceMock } from './config/mocks'; @@ -139,7 +138,6 @@ function createCoreSetupMock({ savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, - auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), metrics: metricsServiceMock.createSetupContract(), getStartServices: jest @@ -152,7 +150,6 @@ function createCoreSetupMock({ function createCoreStartMock() { const mock: MockedKeys = { - auditTrail: auditTrailServiceMock.createStartContract(), capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createStartContract(), @@ -177,7 +174,6 @@ function createInternalCoreSetupMock() { httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), - auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), metrics: metricsServiceMock.createInternalSetupContract(), }; @@ -192,7 +188,6 @@ function createInternalCoreStartMock() { metrics: metricsServiceMock.createInternalStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), - auditTrail: auditTrailServiceMock.createStartContract(), coreUsageData: coreUsageDataServiceMock.createStartContract(), }; return startDeps; @@ -213,7 +208,6 @@ function createCoreRequestHandlerContextMock() { uiSettings: { client: uiSettingsServiceMock.createClient(), }, - auditor: auditTrailServiceMock.createAuditor(), }; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index a8249ed7e3218..22e79741e854c 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -201,7 +201,6 @@ export function createPluginSetupContext( register: deps.uiSettings.register, }, getStartServices: () => plugin.startDependencies, - auditTrail: deps.auditTrail, }; } @@ -250,7 +249,6 @@ export function createPluginStartContext( uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, }, - auditTrail: deps.auditTrail, coreUsageData: deps.coreUsageData, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 20bd102e6f507..7cd8682050e68 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -198,38 +198,6 @@ export interface AssistantAPIClientParams extends GenericParams { path: '/_migration/assistance'; } -// @public -export interface AuditableEvent { - // (undocumented) - message: string; - // (undocumented) - type: string; -} - -// @public -export interface Auditor { - add(event: AuditableEvent): void; - withAuditScope(name: string): void; -} - -// @public -export interface AuditorFactory { - // (undocumented) - asScoped(request: KibanaRequest): Auditor; -} - -// Warning: (ae-missing-release-tag) "AuditTrailSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface AuditTrailSetup { - register(auditor: AuditorFactory): void; -} - -// Warning: (ae-missing-release-tag) "AuditTrailStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type AuditTrailStart = AuditorFactory; - // @public (undocumented) export interface Authenticated extends AuthResultParams { // (undocumented) @@ -499,8 +467,6 @@ export interface CoreServicesUsageData { // @public export interface CoreSetup { - // (undocumented) - auditTrail: AuditTrailSetup; // (undocumented) capabilities: CapabilitiesSetup; // (undocumented) @@ -527,8 +493,6 @@ export interface CoreSetup AuditorFactory, getAuthHeaders?: GetAuthHeaders); + constructor(config: LegacyElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); asScoped(request?: ScopeableRequest): ILegacyScopedClusterClient; callAsInternalUser: LegacyAPICaller; close(): void; @@ -1396,7 +1360,7 @@ export interface LegacyRequest extends Request { // @public @deprecated export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { - constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined, auditor?: Auditor | undefined); + constructor(internalAPICaller: LegacyAPICaller, scopedAPICaller: LegacyAPICaller, headers?: Headers | undefined); callAsCurrentUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } @@ -1738,7 +1702,6 @@ export interface RequestHandlerContext { uiSettings: { client: IUiSettingsClient; }; - auditor: Auditor; }; } diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 77f2787b75412..fe299c6d11675 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -100,9 +100,3 @@ export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); - -import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; -export const mockAuditTrailService = auditTrailServiceMock.create(); -jest.doMock('./audit_trail/audit_trail_service', () => ({ - AuditTrailService: jest.fn(() => mockAuditTrailService), -})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 51defb7d0392e..78703ceeec7ae 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -31,7 +31,6 @@ import { mockMetricsService, mockStatusService, mockLoggingService, - mockAuditTrailService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -71,7 +70,6 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); - expect(mockAuditTrailService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -85,7 +83,6 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); expect(mockStatusService.setup).toHaveBeenCalledTimes(1); expect(mockLoggingService.setup).toHaveBeenCalledTimes(1); - expect(mockAuditTrailService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -126,7 +123,6 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); - expect(mockAuditTrailService.start).not.toHaveBeenCalled(); await server.start(); @@ -135,7 +131,6 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); - expect(mockAuditTrailService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -160,7 +155,6 @@ test('stops services on "stop"', async () => { expect(mockMetricsService.stop).not.toHaveBeenCalled(); expect(mockStatusService.stop).not.toHaveBeenCalled(); expect(mockLoggingService.stop).not.toHaveBeenCalled(); - expect(mockAuditTrailService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -173,7 +167,6 @@ test('stops services on "stop"', async () => { expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); expect(mockStatusService.stop).toHaveBeenCalledTimes(1); expect(mockLoggingService.stop).toHaveBeenCalledTimes(1); - expect(mockAuditTrailService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -227,7 +220,6 @@ test(`doesn't validate config if env.isDevClusterMaster is true`, async () => { expect(mockEnsureValidConfiguration).not.toHaveBeenCalled(); expect(mockContextService.setup).toHaveBeenCalled(); - expect(mockAuditTrailService.setup).toHaveBeenCalled(); expect(mockHttpService.setup).toHaveBeenCalled(); expect(mockElasticsearchService.setup).toHaveBeenCalled(); expect(mockSavedObjectsService.setup).toHaveBeenCalled(); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index f38cac4f43768..eaa03d11cab98 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -21,7 +21,6 @@ import { config as pathConfig } from '@kbn/utils'; import { mapToObject } from '@kbn/std'; import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; import { CoreApp } from './core_app'; -import { AuditTrailService } from './audit_trail'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; @@ -72,7 +71,6 @@ export class Server { private readonly status: StatusService; private readonly logging: LoggingService; private readonly coreApp: CoreApp; - private readonly auditTrail: AuditTrailService; private readonly coreUsageData: CoreUsageDataService; #pluginsInitialized?: boolean; @@ -103,7 +101,6 @@ export class Server { this.status = new StatusService(core); this.coreApp = new CoreApp(core); this.httpResources = new HttpResourcesService(core); - this.auditTrail = new AuditTrailService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); } @@ -139,8 +136,6 @@ export class Server { ]), }); - const auditTrailSetup = this.auditTrail.setup(); - const httpSetup = await this.http.setup({ context: contextServiceSetup, }); @@ -200,7 +195,6 @@ export class Server { uiSettings: uiSettingsSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, - auditTrail: auditTrailSetup, logging: loggingSetup, metrics: metricsSetup, }; @@ -225,11 +219,7 @@ export class Server { this.log.debug('starting server'); const startTransaction = apm.startTransaction('server_start', 'kibana_platform'); - const auditTrailStart = this.auditTrail.start(); - - const elasticsearchStart = await this.elasticsearch.start({ - auditTrail: auditTrailStart, - }); + const elasticsearchStart = await this.elasticsearch.start(); const soStartSpan = startTransaction?.startSpan('saved_objects.migration', 'migration'); const savedObjectsStart = await this.savedObjects.start({ elasticsearch: elasticsearchStart, @@ -252,7 +242,6 @@ export class Server { metrics: metricsStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, - auditTrail: auditTrailStart, coreUsageData: coreUsageDataStart, }; @@ -285,7 +274,6 @@ export class Server { await this.metrics.stop(); await this.status.stop(); await this.logging.stop(); - await this.auditTrail.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.ts b/x-pack/plugins/actions/server/authorization/audit_logger.ts index 7e0adc9206656..3bbf60b0b3ed4 100644 --- a/x-pack/plugins/actions/server/authorization/audit_logger.ts +++ b/x-pack/plugins/actions/server/authorization/audit_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../../../security/server'; export enum AuthorizationResult { Unauthorized = 'Unauthorized', @@ -12,9 +12,9 @@ export enum AuthorizationResult { } export class ActionsAuthorizationAuditLogger { - private readonly auditLogger: AuditLogger; + private readonly auditLogger: LegacyAuditLogger; - constructor(auditLogger: AuditLogger = { log() {} }) { + constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index ac91d689798c9..d747efbb959d8 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -20,7 +20,7 @@ import { securityMock } from '../../security/server/mocks'; import { PluginStartContract as ActionsStartContract } from '../../actions/server'; import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; -import { AuditLogger } from '../../security/server'; +import { LegacyAuditLogger } from '../../security/server'; import { ALERTS_FEATURE_ID } from '../common'; import { eventLogMock } from '../../event_log/server/mocks'; @@ -85,7 +85,7 @@ test('creates an alerts client with proper constructor arguments when security i const logger = { log: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; securityPluginSetup.audit.getLogger.mockReturnValue(logger); factory.create(request, savedObjectsService); diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.ts index f930da2ce428c..7f259df717468 100644 --- a/x-pack/plugins/alerts/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../../../security/server'; export enum ScopeType { Consumer, @@ -17,9 +17,9 @@ export enum AuthorizationResult { } export class AlertsAuthorizationAuditLogger { - private readonly auditLogger: AuditLogger; + private readonly auditLogger: LegacyAuditLogger; - constructor(auditLogger: AuditLogger = { log() {} }) { + constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } diff --git a/x-pack/plugins/audit_trail/kibana.json b/x-pack/plugins/audit_trail/kibana.json deleted file mode 100644 index ce92e232ec13b..0000000000000 --- a/x-pack/plugins/audit_trail/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "auditTrail", - "version": "8.0.0", - "kibanaVersion": "kibana", - "configPath": ["xpack", "audit_trail"], - "server": true, - "ui": false, - "requiredPlugins": ["licensing", "security"], - "optionalPlugins": ["spaces"] -} diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts deleted file mode 100644 index 76ca3e56fe837..0000000000000 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Subject } from 'rxjs'; - -import { AuditTrailClient } from './audit_trail_client'; -import { AuditEvent } from '../types'; - -import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { securityMock } from '../../../security/server/mocks'; -import { spacesMock } from '../../../spaces/server/mocks'; - -describe('AuditTrailClient', () => { - let client: AuditTrailClient; - let event$: Subject; - const deps = { - getCurrentUser: securityMock.createSetup().authc.getCurrentUser, - getSpaceId: spacesMock.createSetup().spacesService.getSpaceId, - }; - - beforeEach(() => { - event$ = new Subject(); - client = new AuditTrailClient( - httpServerMock.createKibanaRequest({ - kibanaRequestState: { requestId: 'request id alpha', requestUuid: 'ignore-me' }, - }), - event$, - deps - ); - }); - - afterEach(() => { - event$.complete(); - }); - - describe('#withAuditScope', () => { - it('registers upper level scope', (done) => { - client.withAuditScope('scope_name'); - event$.subscribe((event) => { - expect(event.scope).toBe('scope_name'); - done(); - }); - client.add({ message: 'message', type: 'type' }); - }); - - it('populates requestId', (done) => { - client.withAuditScope('scope_name'); - event$.subscribe((event) => { - expect(event.requestId).toBe('request id alpha'); - done(); - }); - client.add({ message: 'message', type: 'type' }); - }); - - it('throws an exception if tries to re-write a scope', () => { - client.withAuditScope('scope_name'); - expect(() => client.withAuditScope('another_scope_name')).toThrowErrorMatchingInlineSnapshot( - `"Audit scope is already set to: scope_name"` - ); - }); - }); -}); diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts deleted file mode 100644 index e5022234af9d7..0000000000000 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Subject } from 'rxjs'; -import { KibanaRequest, Auditor, AuditableEvent } from 'src/core/server'; -import { AuditEvent } from '../types'; - -import { SecurityPluginSetup } from '../../../security/server'; -import { SpacesPluginSetup } from '../../../spaces/server'; - -interface Deps { - getCurrentUser: SecurityPluginSetup['authc']['getCurrentUser']; - getSpaceId?: SpacesPluginSetup['spacesService']['getSpaceId']; -} - -export class AuditTrailClient implements Auditor { - private scope?: string; - constructor( - private readonly request: KibanaRequest, - private readonly event$: Subject, - private readonly deps: Deps - ) {} - - public withAuditScope(name: string) { - if (this.scope !== undefined) { - throw new Error(`Audit scope is already set to: ${this.scope}`); - } - this.scope = name; - } - - public add(event: AuditableEvent) { - const user = this.deps.getCurrentUser(this.request); - // doesn't use getSpace since it's async operation calling ES - const spaceId = this.deps.getSpaceId ? this.deps.getSpaceId(this.request) : undefined; - - this.event$.next({ - message: event.message, - type: event.type, - user: user?.username, - space: spaceId, - scope: this.scope, - requestId: this.request.id, - }); - } -} diff --git a/x-pack/plugins/audit_trail/server/config.test.ts b/x-pack/plugins/audit_trail/server/config.test.ts deleted file mode 100644 index 65dfc9f589ec9..0000000000000 --- a/x-pack/plugins/audit_trail/server/config.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { config } from './config'; - -describe('config schema', () => { - it('generates proper defaults', () => { - expect(config.schema.validate({})).toEqual({ - enabled: false, - logger: { - enabled: false, - }, - }); - }); - - it('accepts an appender', () => { - const appender = config.schema.validate({ - appender: { - kind: 'file', - path: '/path/to/file.txt', - layout: { - kind: 'json', - }, - }, - logger: { - enabled: false, - }, - }).appender; - - expect(appender).toEqual({ - kind: 'file', - path: '/path/to/file.txt', - layout: { - kind: 'json', - }, - }); - }); - - it('rejects an appender if not fully configured', () => { - expect(() => - config.schema.validate({ - // no layout configured - appender: { - kind: 'file', - path: '/path/to/file.txt', - }, - logger: { - enabled: false, - }, - }) - ).toThrow(); - }); -}); diff --git a/x-pack/plugins/audit_trail/server/config.ts b/x-pack/plugins/audit_trail/server/config.ts deleted file mode 100644 index 7b05c04c2236f..0000000000000 --- a/x-pack/plugins/audit_trail/server/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginConfigDescriptor, config as coreConfig } from '../../../../src/core/server'; - -const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), - appender: schema.maybe(coreConfig.logging.appenders), - logger: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), -}); - -export type AuditTrailConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - schema: configSchema, -}; diff --git a/x-pack/plugins/audit_trail/server/index.ts b/x-pack/plugins/audit_trail/server/index.ts deleted file mode 100644 index 7db48823a0e29..0000000000000 --- a/x-pack/plugins/audit_trail/server/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from 'src/core/server'; -import { AuditTrailPlugin } from './plugin'; - -export { config } from './config'; -export const plugin = (initializerContext: PluginInitializerContext) => { - return new AuditTrailPlugin(initializerContext); -}; diff --git a/x-pack/plugins/audit_trail/server/plugin.test.ts b/x-pack/plugins/audit_trail/server/plugin.test.ts deleted file mode 100644 index fa5fd1bcc1e14..0000000000000 --- a/x-pack/plugins/audit_trail/server/plugin.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { first } from 'rxjs/operators'; -import { AuditTrailPlugin } from './plugin'; -import { coreMock } from '../../../../src/core/server/mocks'; - -import { securityMock } from '../../security/server/mocks'; -import { spacesMock } from '../../spaces/server/mocks'; - -describe('AuditTrail plugin', () => { - describe('#setup', () => { - let plugin: AuditTrailPlugin; - let pluginInitContextMock: ReturnType; - let coreSetup: ReturnType; - - const deps = { - security: securityMock.createSetup(), - spaces: spacesMock.createSetup(), - }; - - beforeEach(() => { - pluginInitContextMock = coreMock.createPluginInitializerContext(); - plugin = new AuditTrailPlugin(pluginInitContextMock); - coreSetup = coreMock.createSetup(); - }); - - afterEach(async () => { - await plugin.stop(); - }); - - it('registers AuditTrail factory', async () => { - pluginInitContextMock = coreMock.createPluginInitializerContext(); - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - expect(coreSetup.auditTrail.register).toHaveBeenCalledTimes(1); - }); - - describe('logger', () => { - it('registers a custom logger', async () => { - pluginInitContextMock = coreMock.createPluginInitializerContext(); - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - expect(coreSetup.logging.configure).toHaveBeenCalledTimes(1); - }); - - it('disables logging if config.logger.enabled: false', async () => { - const config = { - logger: { - enabled: false, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.loggers?.every((l) => l.level === 'off')).toBe(true); - }); - it('logs with DEBUG level if config.logger.enabled: true', async () => { - const config = { - logger: { - enabled: true, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.loggers?.every((l) => l.level === 'debug')).toBe(true); - }); - it('uses appender adjusted via config', async () => { - const config = { - appender: { - kind: 'file', - path: '/path/to/file.txt', - }, - logger: { - enabled: true, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.appenders).toEqual({ auditTrailAppender: config.appender }); - }); - it('falls back to the default appender if not configured', async () => { - const config = { - logger: { - enabled: true, - }, - }; - pluginInitContextMock = coreMock.createPluginInitializerContext(config); - - plugin = new AuditTrailPlugin(pluginInitContextMock); - plugin.setup(coreSetup, deps); - - const args = coreSetup.logging.configure.mock.calls[0][0]; - const value = await args.pipe(first()).toPromise(); - expect(value.appenders).toEqual({ - auditTrailAppender: { - kind: 'console', - layout: { - kind: 'pattern', - highlight: true, - }, - }, - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/audit_trail/server/plugin.ts b/x-pack/plugins/audit_trail/server/plugin.ts deleted file mode 100644 index cf423f230aef9..0000000000000 --- a/x-pack/plugins/audit_trail/server/plugin.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Observable, Subject } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { - AppenderConfigType, - CoreSetup, - CoreStart, - KibanaRequest, - Logger, - LoggerContextConfigInput, - Plugin, - PluginInitializerContext, -} from 'src/core/server'; - -import { AuditEvent } from './types'; -import { AuditTrailClient } from './client/audit_trail_client'; -import { AuditTrailConfigType } from './config'; - -import { SecurityPluginSetup } from '../../security/server'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { LicensingPluginStart } from '../../licensing/server'; - -interface DepsSetup { - security: SecurityPluginSetup; - spaces?: SpacesPluginSetup; -} - -interface DepStart { - licensing: LicensingPluginStart; -} - -export class AuditTrailPlugin implements Plugin { - private readonly logger: Logger; - private readonly config$: Observable; - private readonly event$ = new Subject(); - - constructor(private readonly context: PluginInitializerContext) { - this.logger = this.context.logger.get(); - this.config$ = this.context.config.create(); - } - - public setup(core: CoreSetup, deps: DepsSetup) { - const depsApi = { - getCurrentUser: deps.security.authc.getCurrentUser, - getSpaceId: deps.spaces?.spacesService.getSpaceId, - }; - - this.event$.subscribe(({ message, ...other }) => this.logger.debug(message, other)); - - core.auditTrail.register({ - asScoped: (request: KibanaRequest) => { - return new AuditTrailClient(request, this.event$, depsApi); - }, - }); - - core.logging.configure( - this.config$.pipe( - map((config) => ({ - appenders: { - auditTrailAppender: this.getAppender(config), - }, - loggers: [ - { - // plugins.auditTrail prepended automatically - context: '', - // do not pipe in root log if disabled - level: config.logger.enabled ? 'debug' : 'off', - appenders: ['auditTrailAppender'], - }, - ], - })) - ) - ); - } - - private getAppender(config: AuditTrailConfigType): AppenderConfigType { - return ( - config.appender ?? { - kind: 'console', - layout: { - kind: 'pattern', - highlight: true, - }, - } - ); - } - - public start(core: CoreStart, deps: DepStart) {} - public stop() { - this.event$.complete(); - } -} diff --git a/x-pack/plugins/audit_trail/server/types.ts b/x-pack/plugins/audit_trail/server/types.ts deleted file mode 100644 index 1b7afb09f0629..0000000000000 --- a/x-pack/plugins/audit_trail/server/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -/** - * Event enhanced with request context data. Provided to an external consumer. - * @public - */ -export interface AuditEvent { - message: string; - type: string; - scope?: string; - user?: string; - space?: string; - requestId?: string; -} diff --git a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts index de14a79dd0ddb..4f3e7e9f2b5a8 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger, AuthenticatedUser } from '../../../security/server'; +import { LegacyAuditLogger, AuthenticatedUser } from '../../../security/server'; import { SavedObjectDescriptor, descriptorToArray } from '../crypto'; /** * Represents all audit events the plugin can log. */ export class EncryptedSavedObjectsAuditLogger { - constructor(private readonly logger: AuditLogger = { log() {} }) {} + constructor(private readonly logger: LegacyAuditLogger = { log() {} }) {} public encryptAttributeFailure( attributeName: string, diff --git a/x-pack/plugins/security/README.md b/x-pack/plugins/security/README.md index 068f19ba9482b..b93be0269536b 100644 --- a/x-pack/plugins/security/README.md +++ b/x-pack/plugins/security/README.md @@ -1,3 +1,92 @@ # Kibana Security Plugin -See [Configuring security in Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html). +See [Configuring security in +Kibana](https://www.elastic.co/guide/en/kibana/current/using-kibana-with-security.html). + +## Audit logging + +### Example + +```typescript +const auditLogger = securitySetup.audit.asScoped(request); +auditLogger.log({ + message: 'User is updating dashboard [id=123]', + event: { + action: 'saved_object_update', + category: EventCategory.DATABASE, + type: EventType.CHANGE, + outcome: EventOutcome.UNKNOWN, + }, + kibana: { + saved_object: { type: 'dashboard', id: '123' }, + }, +}); +``` + +### What events should be logged? + +The purpose of an audit log is to support compliance, accountability and +security by capturing who performed an action, what action was performed and +when it occurred. It is not the purpose of an audit log to aid with debugging +the system or provide usage statistics. + +**Kibana guidelines:** + +Each API call to Kibana will result in a record in the audit log that captures +general information about the request (`http_request` event). + +In addition to that, any operation that is performed on a resource owned by +Kibana (e.g. saved objects) and that falls in the following categories, should +be included in the audit log: + +- System access (incl. failed attempts due to authentication errors) +- Data reads (incl. failed attempts due to authorisation errors) +- Data writes (incl. failed attempts due to authorisation errors) + +If Kibana does not own the resource (e.g. when running queries against user +indices), then auditing responsibilities are deferred to Elasticsearch and no +additional events will be logged. + +**Examples:** + +For a list of audit events that Kibana currently logs see: +`docs/user/security/audit-logging.asciidoc` + +### When should an event be logged? + +Due to the asynchronous nature of most operations in Kibana, there is an +inherent tradeoff between the following logging approaches: + +- Logging the **intention** before performing an operation, leading to false + positives if the operation fails downstream. +- Logging the **outcome** after completing an operation, leading to missing + records if Kibana crashes before the response is received. +- Logging **both**, intention and outcome, leading to unnecessary duplication + and noisy/difficult to analyse logs. + +**Kibana guidelines:** + +- **Write operations** should be logged immediately after all authorisation + checks have passed, but before the response is received (logging the + intention). This ensures that a record of every operation is persisted even in + case of an unexpected error. +- **Read operations**, on the other hand, should be logged after the operation + completed (logging the outcome) since we won't know what resources were + accessed before receiving the response. +- Be explicit about the timing and outcome of an action in your messaging. (e.g. + "User has logged in" vs. "User is creating dashboard") + +### Can an action trigger multiple events? + +- A request to Kibana can perform a combination of different operations, each of + which should be captured as separate events. +- Operations that are performed on multiple resources (**bulk operations**) + should be logged as separate events, one for each resource. +- Actions that kick off **background tasks** should be logged as separate + events, one for creating the task and another one for executing it. +- **Internal checks**, which have been carried out in order to perform an + operation, or **errors** that occured as a result of an operation should be + logged as an outcome of the operation itself, using the ECS `event.outcome` + and `error` fields, instead of logging a separate event. +- Multiple events that were part of the same request can be correlated in the + audit log using the ECS `trace.id` property. diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts new file mode 100644 index 0000000000000..ae40429eea1b6 --- /dev/null +++ b/x-pack/plugins/security/server/audit/audit_events.test.ts @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EventOutcome, + SavedObjectAction, + savedObjectEvent, + userLoginEvent, + httpRequestEvent, +} from './audit_events'; +import { AuthenticationResult } from '../authentication'; +import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { httpServerMock } from 'src/core/server/mocks'; + +describe('#savedObjectEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "saved_object_create", + "category": "database", + "outcome": "unknown", + "type": "creation", + }, + "kibana": Object { + "add_to_spaces": undefined, + "delete_from_spaces": undefined, + "saved_object": Object { + "id": "SAVED_OBJECT_ID", + "type": "dashboard", + }, + }, + "message": "User is creating dashboard [id=SAVED_OBJECT_ID]", + } + `); + }); + + test('creates event with `success` outcome', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "saved_object_create", + "category": "database", + "outcome": "success", + "type": "creation", + }, + "kibana": Object { + "add_to_spaces": undefined, + "delete_from_spaces": undefined, + "saved_object": Object { + "id": "SAVED_OBJECT_ID", + "type": "dashboard", + }, + }, + "message": "User has created dashboard [id=SAVED_OBJECT_ID]", + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type: 'dashboard', id: 'SAVED_OBJECT_ID' }, + error: new Error('ERROR_MESSAGE'), + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "ERROR_MESSAGE", + }, + "event": Object { + "action": "saved_object_create", + "category": "database", + "outcome": "failure", + "type": "creation", + }, + "kibana": Object { + "add_to_spaces": undefined, + "delete_from_spaces": undefined, + "saved_object": Object { + "id": "SAVED_OBJECT_ID", + "type": "dashboard", + }, + }, + "message": "Failed attempt to create dashboard [id=SAVED_OBJECT_ID]", + } + `); + }); +}); + +describe('#userLoginEvent', () => { + test('creates event with `success` outcome', () => { + expect( + userLoginEvent({ + authenticationResult: AuthenticationResult.succeeded(mockAuthenticatedUser()), + authenticationProvider: 'basic1', + authenticationType: 'basic', + }) + ).toMatchInlineSnapshot(` + Object { + "error": undefined, + "event": Object { + "action": "user_login", + "category": "authentication", + "outcome": "success", + }, + "kibana": Object { + "authentication_provider": "basic1", + "authentication_realm": "native1", + "authentication_type": "basic", + "lookup_realm": "native1", + "space_id": undefined, + }, + "message": "User [user] has logged in using basic provider [name=basic1]", + "user": Object { + "name": "user", + "roles": Array [ + "user-role", + ], + }, + } + `); + }); + + test('creates event with `failure` outcome', () => { + expect( + userLoginEvent({ + authenticationResult: AuthenticationResult.failed(new Error('Not Authorized')), + authenticationProvider: 'basic1', + authenticationType: 'basic', + }) + ).toMatchInlineSnapshot(` + Object { + "error": Object { + "code": "Error", + "message": "Not Authorized", + }, + "event": Object { + "action": "user_login", + "category": "authentication", + "outcome": "failure", + }, + "kibana": Object { + "authentication_provider": "basic1", + "authentication_realm": undefined, + "authentication_type": "basic", + "lookup_realm": undefined, + "space_id": undefined, + }, + "message": "Failed attempt to login using basic provider [name=basic1]", + "user": undefined, + } + `); + }); +}); + +describe('#httpRequestEvent', () => { + test('creates event with `unknown` outcome', () => { + expect( + httpRequestEvent({ + request: httpServerMock.createKibanaRequest(), + }) + ).toMatchInlineSnapshot(` + Object { + "event": Object { + "action": "http_request", + "category": "web", + "outcome": "unknown", + }, + "http": Object { + "request": Object { + "method": "get", + }, + }, + "message": "User is requesting [/path] endpoint", + "url": Object { + "domain": undefined, + "path": "/path", + "port": undefined, + "query": undefined, + "scheme": undefined, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts new file mode 100644 index 0000000000000..48a3b1e7e85b0 --- /dev/null +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -0,0 +1,244 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'src/core/server'; +import { AuthenticationResult } from '../authentication/authentication_result'; + +/** + * Audit event schema using ECS format. + * https://www.elastic.co/guide/en/ecs/1.5/index.html + * @public + */ +export interface AuditEvent { + /** + * Human readable message describing action, outcome and user. + * + * @example + * User [jdoe] logged in using basic provider [name=basic1] + */ + message: string; + event: { + action: string; + category?: EventCategory; + type?: EventType; + outcome?: EventOutcome; + module?: string; + dataset?: string; + }; + user?: { + name: string; + email?: string; + full_name?: string; + hash?: string; + roles?: readonly string[]; + }; + kibana?: { + /** + * Current space id of the request. + */ + space_id?: string; + /** + * Saved object that was created, changed, deleted or accessed as part of the action. + */ + saved_object?: { + type: string; + id?: string; + }; + /** + * Any additional event specific fields. + */ + [x: string]: any; + }; + error?: { + code?: string; + message?: string; + }; + http?: { + request?: { + method?: string; + body?: { + content: string; + }; + }; + response?: { + status_code?: number; + }; + }; + url?: { + domain?: string; + full?: string; + path?: string; + port?: number; + query?: string; + scheme?: string; + }; +} + +export enum EventCategory { + DATABASE = 'database', + WEB = 'web', + IAM = 'iam', + AUTHENTICATION = 'authentication', + PROCESS = 'process', +} + +export enum EventType { + USER = 'user', + GROUP = 'group', + CREATION = 'creation', + ACCESS = 'access', + CHANGE = 'change', + DELETION = 'deletion', +} + +export enum EventOutcome { + SUCCESS = 'success', + FAILURE = 'failure', + UNKNOWN = 'unknown', +} + +export interface HttpRequestParams { + request: KibanaRequest; +} + +export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent { + const { pathname, search } = request.url; + + return { + message: `User is requesting [${pathname}] endpoint`, + event: { + action: 'http_request', + category: EventCategory.WEB, + outcome: EventOutcome.UNKNOWN, + }, + http: { + request: { + method: request.route.method, + }, + }, + url: { + domain: request.url.hostname, + path: pathname, + port: request.url.port ? parseInt(request.url.port, 10) : undefined, + query: search?.slice(1) || undefined, + scheme: request.url.protocol, + }, + }; +} + +export interface UserLoginParams { + authenticationResult: AuthenticationResult; + authenticationProvider?: string; + authenticationType?: string; +} + +export function userLoginEvent({ + authenticationResult, + authenticationProvider, + authenticationType, +}: UserLoginParams): AuditEvent { + return { + message: authenticationResult.user + ? `User [${authenticationResult.user.username}] has logged in using ${authenticationType} provider [name=${authenticationProvider}]` + : `Failed attempt to login using ${authenticationType} provider [name=${authenticationProvider}]`, + event: { + action: 'user_login', + category: EventCategory.AUTHENTICATION, + outcome: authenticationResult.user ? EventOutcome.SUCCESS : EventOutcome.FAILURE, + }, + user: authenticationResult.user && { + name: authenticationResult.user.username, + roles: authenticationResult.user.roles, + }, + kibana: { + space_id: undefined, // Ensure this does not get populated by audit service + authentication_provider: authenticationProvider, + authentication_type: authenticationType, + authentication_realm: authenticationResult.user?.authentication_realm.name, + lookup_realm: authenticationResult.user?.lookup_realm.name, + }, + error: authenticationResult.error && { + code: authenticationResult.error.name, + message: authenticationResult.error.message, + }, + }; +} + +export enum SavedObjectAction { + CREATE = 'saved_object_create', + GET = 'saved_object_get', + UPDATE = 'saved_object_update', + DELETE = 'saved_object_delete', + FIND = 'saved_object_find', + ADD_TO_SPACES = 'saved_object_add_to_spaces', + DELETE_FROM_SPACES = 'saved_object_delete_from_spaces', +} + +const eventVerbs = { + saved_object_create: ['create', 'creating', 'created'], + saved_object_get: ['access', 'accessing', 'accessed'], + saved_object_update: ['update', 'updating', 'updated'], + saved_object_delete: ['delete', 'deleting', 'deleted'], + saved_object_find: ['access', 'accessing', 'accessed'], + saved_object_add_to_spaces: ['update', 'updating', 'updated'], + saved_object_delete_from_spaces: ['update', 'updating', 'updated'], +}; + +const eventTypes = { + saved_object_create: EventType.CREATION, + saved_object_get: EventType.ACCESS, + saved_object_update: EventType.CHANGE, + saved_object_delete: EventType.DELETION, + saved_object_find: EventType.ACCESS, + saved_object_add_to_spaces: EventType.CHANGE, + saved_object_delete_from_spaces: EventType.CHANGE, +}; + +export interface SavedObjectParams { + action: SavedObjectAction; + outcome?: EventOutcome; + savedObject?: Required['kibana']>['saved_object']; + addToSpaces?: readonly string[]; + deleteFromSpaces?: readonly string[]; + error?: Error; +} + +export function savedObjectEvent({ + action, + savedObject, + addToSpaces, + deleteFromSpaces, + outcome, + error, +}: SavedObjectParams): AuditEvent { + const doc = savedObject ? `${savedObject.type} [id=${savedObject.id}]` : 'saved objects'; + const [present, progressive, past] = eventVerbs[action]; + const message = error + ? `Failed attempt to ${present} ${doc}` + : outcome === 'unknown' + ? `User is ${progressive} ${doc}` + : `User has ${past} ${doc}`; + const type = eventTypes[action]; + + return { + message, + event: { + action, + category: EventCategory.DATABASE, + type, + outcome: outcome ?? (error ? EventOutcome.FAILURE : EventOutcome.SUCCESS), + }, + kibana: { + saved_object: savedObject, + add_to_spaces: addToSpaces, + delete_from_spaces: deleteFromSpaces, + }, + error: error && { + code: error.name, + message: error.message, + }, + }; +} diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index b2d866d07ff89..60dbe341dc4bf 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -3,163 +3,506 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AuditService } from './audit_service'; -import { loggingSystemMock } from 'src/core/server/mocks'; +import { AuditService, filterEvent, createLoggingConfig } from './audit_service'; +import { AuditEvent, EventCategory, EventType, EventOutcome } from './audit_events'; +import { + coreMock, + loggingSystemMock, + httpServiceMock, + httpServerMock, +} from 'src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { ConfigSchema, ConfigType } from '../config'; import { SecurityLicenseFeatures } from '../../common/licensing'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; const createConfig = (settings: Partial) => { return ConfigSchema.validate(settings); }; -const config = createConfig({ - enabled: true, +const logger = loggingSystemMock.createLogger(); +const license = licenseMock.create(); +const config = createConfig({ enabled: true }); +const { logging } = coreMock.createSetup(); +const http = httpServiceMock.createSetupContract(); +const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] }); +const getSpaceId = jest.fn().mockReturnValue('default'); + +beforeEach(() => { + logger.info.mockClear(); + logging.configure.mockClear(); + http.registerOnPostAuth.mockClear(); }); describe('#setup', () => { it('returns the expected contract', () => { - const logger = loggingSystemMock.createLogger(); const auditService = new AuditService(logger); - const license = licenseMock.create(); - expect(auditService.setup({ license, config })).toMatchInlineSnapshot(` + expect( + auditService.setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }) + ).toMatchInlineSnapshot(` Object { + "asScoped": [Function], "getLogger": [Function], } `); }); -}); - -test(`calls the underlying logger with the provided message and requisite tags`, () => { - const pluginId = 'foo'; - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: true, - } as SecurityLicenseFeatures).asObservable(); + it('configures logging correctly when using ecs logger', async () => { + new AuditService(logger).setup({ + license, + config: { + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }, + logging, + http, + getCurrentUser, + getSpaceId, + }); + expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable)); + }); - const auditService = new AuditService(logger).setup({ license, config }); + it('does not configure logging when using legacy logger', async () => { + new AuditService(logger).setup({ + license, + config: { + enabled: true, + }, + logging, + http, + getCurrentUser, + getSpaceId, + }); + expect(logging.configure).not.toHaveBeenCalled(); + }); - const auditLogger = auditService.getLogger(pluginId); + it('registers post auth hook', () => { + new AuditService(logger).setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function)); + }); +}); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); +describe('#asScoped', () => { + it('logs event enriched with meta data', async () => { + const audit = new AuditService(logger).setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + const request = httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, + }); + + audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + expect(logger.info).toHaveBeenCalledWith('MESSAGE', { + event: { action: 'ACTION' }, + kibana: { space_id: 'default' }, + message: 'MESSAGE', + trace: { id: 'REQUEST_ID' }, + user: { name: 'jdoe', roles: ['admin'] }, + }); + }); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.info).toHaveBeenCalledWith(message, { - eventType, - tags: [pluginId, eventType], + it('does not log to audit logger if event matches ignore filter', async () => { + const audit = new AuditService(logger).setup({ + license, + config: { + enabled: true, + ignore_filters: [{ actions: ['ACTION'] }], + }, + logging, + http, + getCurrentUser, + getSpaceId, + }); + const request = httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, + }); + + audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + expect(logger.info).not.toHaveBeenCalled(); }); }); -test(`calls the underlying logger with the provided metadata`, () => { - const pluginId = 'foo'; +describe('#createLoggingConfig', () => { + test('sets log level to `info` when audit logging is enabled and appender is defined', async () => { + const features$ = of({ + allowAuditLogging: true, + }); + + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }) + ) + .toPromise(); + + expect(loggingConfig).toMatchInlineSnapshot(` + Object { + "appenders": Object { + "auditTrailAppender": Object { + "kind": "console", + "layout": Object { + "kind": "pattern", + }, + }, + }, + "loggers": Array [ + Object { + "appenders": Array [ + "auditTrailAppender", + ], + "context": "audit", + "level": "info", + }, + ], + } + `); + }); - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: true, - } as SecurityLicenseFeatures).asObservable(); + test('sets log level to `off` when audit logging is disabled', async () => { + const features$ = of({ + allowAuditLogging: true, + }); + + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: false, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }) + ) + .toPromise(); + + expect(loggingConfig.loggers![0].level).toEqual('off'); + }); - const auditService = new AuditService(logger).setup({ license, config }); + test('sets log level to `off` when appender is not defined', async () => { + const features$ = of({ + allowAuditLogging: true, + }); - const auditLogger = auditService.getLogger(pluginId); + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: true, + }) + ) + .toPromise(); - const eventType = 'bar'; - const message = 'this is my audit message'; - const metadata = Object.freeze({ - property1: 'value1', - property2: false, - property3: 123, + expect(loggingConfig.loggers![0].level).toEqual('off'); }); - auditLogger.log(eventType, message, metadata); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.info).toHaveBeenCalledWith(message, { - eventType, - tags: [pluginId, eventType], - property1: 'value1', - property2: false, - property3: 123, + test('sets log level to `off` when license does not allow audit logging', async () => { + const features$ = of({ + allowAuditLogging: false, + }); + + const loggingConfig = await features$ + .pipe( + createLoggingConfig({ + enabled: true, + appender: { + kind: 'console', + layout: { + kind: 'pattern', + }, + }, + }) + ) + .toPromise(); + + expect(loggingConfig.loggers![0].level).toEqual('off'); }); }); -test(`does not call the underlying logger if license does not support audit logging`, () => { - const pluginId = 'foo'; +describe('#filterEvent', () => { + const event: AuditEvent = { + message: 'this is my audit message', + event: { + action: 'http_request', + category: EventCategory.WEB, + type: EventType.ACCESS, + outcome: EventOutcome.SUCCESS, + }, + user: { + name: 'jdoe', + }, + kibana: { + space_id: 'default', + }, + }; + + test('keeps event when ignore filters are undefined or empty', () => { + expect(filterEvent(event, undefined)).toBeTruthy(); + expect(filterEvent(event, [])).toBeTruthy(); + }); - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: false, - } as SecurityLicenseFeatures).asObservable(); + test('filters event correctly when a single match is found per criteria', () => { + expect(filterEvent(event, [{ actions: ['NO_MATCH'] }])).toBeTruthy(); + expect(filterEvent(event, [{ actions: ['NO_MATCH', 'http_request'] }])).toBeFalsy(); + expect(filterEvent(event, [{ categories: ['NO_MATCH', 'web'] }])).toBeFalsy(); + expect(filterEvent(event, [{ types: ['NO_MATCH', 'access'] }])).toBeFalsy(); + expect(filterEvent(event, [{ outcomes: ['NO_MATCH', 'success'] }])).toBeFalsy(); + expect(filterEvent(event, [{ spaces: ['NO_MATCH', 'default'] }])).toBeFalsy(); + }); - const auditService = new AuditService(logger).setup({ license, config }); + test('keeps event when one criteria per rule does not match', () => { + expect( + filterEvent(event, [ + { + actions: ['NO_MATCH'], + categories: ['web'], + types: ['access'], + outcomes: ['success'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['NO_MATCH'], + types: ['access'], + outcomes: ['success'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['NO_MATCH'], + outcomes: ['success'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['access'], + outcomes: ['NO_MATCH'], + spaces: ['default'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['access'], + outcomes: ['success'], + spaces: ['NO_MATCH'], + }, + ]) + ).toBeTruthy(); + }); - const auditLogger = auditService.getLogger(pluginId); + test('filters out event when all criteria in a single rule match', () => { + expect( + filterEvent(event, [ + { + actions: ['NO_MATCH'], + categories: ['NO_MATCH'], + types: ['NO_MATCH'], + outcomes: ['NO_MATCH'], + spaces: ['NO_MATCH'], + }, + { + actions: ['http_request'], + categories: ['web'], + types: ['access'], + outcomes: ['success'], + spaces: ['default'], + }, + ]) + ).toBeFalsy(); + }); +}); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); +describe('#getLogger', () => { + test('calls the underlying logger with the provided message and requisite tags', () => { + const pluginId = 'foo'; + + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(message, { + eventType, + tags: [pluginId, eventType], + }); + }); - expect(logger.info).not.toHaveBeenCalled(); -}); + test('calls the underlying logger with the provided metadata', () => { + const pluginId = 'foo'; + + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); + + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); + + const auditLogger = auditService.getLogger(pluginId); + + const eventType = 'bar'; + const message = 'this is my audit message'; + const metadata = Object.freeze({ + property1: 'value1', + property2: false, + property3: 123, + }); + auditLogger.log(eventType, message, metadata); + + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith(message, { + eventType, + tags: [pluginId, eventType], + property1: 'value1', + property2: false, + property3: 123, + }); + }); + + test('does not call the underlying logger if license does not support audit logging', () => { + const pluginId = 'foo'; + + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: false, + } as SecurityLicenseFeatures).asObservable(); -test(`does not call the underlying logger if security audit logging is not enabled`, () => { - const pluginId = 'foo'; + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); - license.features$ = new BehaviorSubject({ - allowAuditLogging: true, - } as SecurityLicenseFeatures).asObservable(); + const auditLogger = auditService.getLogger(pluginId); - const auditService = new AuditService(logger).setup({ - license, - config: createConfig({ - enabled: false, - }), + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).not.toHaveBeenCalled(); }); - const auditLogger = auditService.getLogger(pluginId); + test('does not call the underlying logger if security audit logging is not enabled', () => { + const pluginId = 'foo'; - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); + const licenseWithFeatures = licenseMock.create(); + licenseWithFeatures.features$ = new BehaviorSubject({ + allowAuditLogging: true, + } as SecurityLicenseFeatures).asObservable(); - expect(logger.info).not.toHaveBeenCalled(); -}); + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config: createConfig({ + enabled: false, + }), + logging, + http, + getCurrentUser, + getSpaceId, + }); -test(`calls the underlying logger after license upgrade`, () => { - const pluginId = 'foo'; + const auditLogger = auditService.getLogger(pluginId); - const logger = loggingSystemMock.createLogger(); - const license = licenseMock.create(); + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); + + expect(logger.info).not.toHaveBeenCalled(); + }); - const features$ = new BehaviorSubject({ - allowAuditLogging: false, - } as SecurityLicenseFeatures); + test('calls the underlying logger after license upgrade', () => { + const pluginId = 'foo'; - license.features$ = features$.asObservable(); + const licenseWithFeatures = licenseMock.create(); - const auditService = new AuditService(logger).setup({ license, config }); + const features$ = new BehaviorSubject({ + allowAuditLogging: false, + } as SecurityLicenseFeatures); - const auditLogger = auditService.getLogger(pluginId); + licenseWithFeatures.features$ = features$.asObservable(); - const eventType = 'bar'; - const message = 'this is my audit message'; - auditLogger.log(eventType, message); + const auditService = new AuditService(logger).setup({ + license: licenseWithFeatures, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }); - expect(logger.info).not.toHaveBeenCalled(); + const auditLogger = auditService.getLogger(pluginId); - // perform license upgrade - features$.next({ - allowAuditLogging: true, - } as SecurityLicenseFeatures); + const eventType = 'bar'; + const message = 'this is my audit message'; + auditLogger.log(eventType, message); - auditLogger.log(eventType, message); + expect(logger.info).not.toHaveBeenCalled(); - expect(logger.info).toHaveBeenCalledTimes(1); + // perform license upgrade + features$.next({ + allowAuditLogging: true, + } as SecurityLicenseFeatures); + + auditLogger.log(eventType, message); + + expect(logger.info).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 93e69fd2601e9..b84ad37332b85 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -5,53 +5,181 @@ */ import { Subscription } from 'rxjs'; -import { Logger } from '../../../../../src/core/server'; -import { SecurityLicense } from '../../common/licensing'; +import { map, distinctUntilKeyChanged } from 'rxjs/operators'; +import { + Logger, + LoggingServiceSetup, + KibanaRequest, + HttpServiceSetup, + LoggerContextConfigInput, +} from '../../../../../src/core/server'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing'; import { ConfigType } from '../config'; +import { SpacesPluginSetup } from '../../../spaces/server'; +import { AuditEvent, httpRequestEvent } from './audit_events'; +import { SecurityPluginSetup } from '..'; -export interface AuditLogger { +/** + * @deprecated + */ +export interface LegacyAuditLogger { log: (eventType: string, message: string, data?: Record) => void; } +export interface AuditLogger { + log: (event: AuditEvent) => void; +} + +interface AuditLogMeta extends AuditEvent { + session?: { + id: string; + }; + trace: { + id: string; + }; +} + export interface AuditServiceSetup { - getLogger: (id?: string) => AuditLogger; + asScoped: (request: KibanaRequest) => AuditLogger; + getLogger: (id?: string) => LegacyAuditLogger; } interface AuditServiceSetupParams { license: SecurityLicense; config: ConfigType['audit']; + logging: Pick; + http: Pick; + getCurrentUser( + request: KibanaRequest + ): ReturnType | undefined; + getSpaceId( + request: KibanaRequest + ): ReturnType | undefined; } export class AuditService { + /** + * @deprecated + */ private licenseFeaturesSubscription?: Subscription; - private auditLoggingEnabled = false; + /** + * @deprecated + */ + private allowAuditLogging = false; constructor(private readonly logger: Logger) {} - setup({ license, config }: AuditServiceSetupParams): AuditServiceSetup { - if (config.enabled) { + setup({ + license, + config, + logging, + http, + getCurrentUser, + getSpaceId, + }: AuditServiceSetupParams): AuditServiceSetup { + if (config.enabled && !config.appender) { this.licenseFeaturesSubscription = license.features$.subscribe(({ allowAuditLogging }) => { - this.auditLoggingEnabled = allowAuditLogging; + this.allowAuditLogging = allowAuditLogging; }); } - return { - getLogger: (id?: string): AuditLogger => { - return { - log: (eventType: string, message: string, data?: Record) => { - if (!this.auditLoggingEnabled) { - return; - } - - this.logger.info(message, { - tags: id ? [id, eventType] : [eventType], - eventType, - ...data, - }); + // Do not change logging for legacy logger + if (config.appender) { + // Configure logging during setup and when license changes + logging.configure( + license.features$.pipe( + distinctUntilKeyChanged('allowAuditLogging'), + createLoggingConfig(config) + ) + ); + } + + /** + * Creates an {@link AuditLogger} scoped to the current request. + * + * @example + * ```typescript + * const auditLogger = securitySetup.audit.asScoped(request); + * auditLogger.log(event); + * ``` + */ + const asScoped = (request: KibanaRequest): AuditLogger => { + /** + * Logs an {@link AuditEvent} and automatically adds meta data about the + * current user, space and correlation id. + * + * Guidelines around what events should be logged and how they should be + * structured can be found in: `/x-pack/plugins/security/README.md` + * + * @example + * ```typescript + * const auditLogger = securitySetup.audit.asScoped(request); + * auditLogger.log({ + * message: 'User is updating dashboard [id=123]', + * event: { + * action: 'saved_object_update', + * outcome: 'unknown' + * }, + * kibana: { + * saved_object: { type: 'dashboard', id: '123' } + * }, + * }); + * ``` + */ + const log = (event: AuditEvent) => { + const user = getCurrentUser(request); + const spaceId = getSpaceId(request); + const meta: AuditLogMeta = { + ...event, + user: + (user && { + name: user.username, + roles: user.roles, + }) || + event.user, + kibana: { + space_id: spaceId, + ...event.kibana, + }, + trace: { + id: request.id, }, }; - }, + if (filterEvent(meta, config.ignore_filters)) { + this.logger.info(event.message!, meta); + } + }; + return { log }; }; + + /** + * @deprecated + * Use `audit.asScoped(request)` method instead to create an audit logger + */ + const getLogger = (id?: string): LegacyAuditLogger => { + return { + log: (eventType: string, message: string, data?: Record) => { + if (!this.allowAuditLogging) { + return; + } + + this.logger.info(message, { + tags: id ? [id, eventType] : [eventType], + eventType, + ...data, + }); + }, + }; + }; + + http.registerOnPostAuth((request, response, t) => { + if (request.auth.isAuthenticated) { + asScoped(request).log(httpRequestEvent({ request })); + } + return t.next(); + }); + + return { asScoped, getLogger }; } stop() { @@ -61,3 +189,40 @@ export class AuditService { } } } + +export const createLoggingConfig = (config: ConfigType['audit']) => + map, LoggerContextConfigInput>((features) => ({ + appenders: { + auditTrailAppender: config.appender ?? { + kind: 'console', + layout: { + kind: 'pattern', + highlight: true, + }, + }, + }, + loggers: [ + { + context: 'audit', + level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off', + appenders: ['auditTrailAppender'], + }, + ], + })); + +export function filterEvent( + event: AuditEvent, + ignoreFilters: ConfigType['audit']['ignore_filters'] +) { + if (ignoreFilters) { + return !ignoreFilters.some( + (rule) => + (!rule.actions || rule.actions.includes(event.event.action)) && + (!rule.categories || rule.categories.includes(event.event.category!)) && + (!rule.types || rule.types.includes(event.event.type!)) && + (!rule.outcomes || rule.outcomes.includes(event.event.outcome!)) && + (!rule.spaces || rule.spaces.includes(event.kibana?.space_id!)) + ); + } + return true; +} diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts index 07341cc06e889..cf95fbbffa962 100644 --- a/x-pack/plugins/security/server/audit/index.mock.ts +++ b/x-pack/plugins/security/server/audit/index.mock.ts @@ -21,6 +21,9 @@ export const auditServiceMock = { create() { return { getLogger: jest.fn(), + asScoped: jest.fn().mockReturnValue({ + log: jest.fn(), + }), } as jest.Mocked>; }, }; diff --git a/x-pack/plugins/security/server/audit/index.ts b/x-pack/plugins/security/server/audit/index.ts index 3db160c703e34..09f3df8b310e7 100644 --- a/x-pack/plugins/security/server/audit/index.ts +++ b/x-pack/plugins/security/server/audit/index.ts @@ -4,5 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AuditService, AuditServiceSetup, AuditLogger } from './audit_service'; +export { AuditService, AuditServiceSetup, AuditLogger, LegacyAuditLogger } from './audit_service'; +export { + AuditEvent, + EventCategory, + EventType, + EventOutcome, + userLoginEvent, + httpRequestEvent, + savedObjectEvent, + SavedObjectAction, +} from './audit_events'; export { SecurityAuditLogger } from './security_audit_logger'; diff --git a/x-pack/plugins/security/server/audit/security_audit_logger.ts b/x-pack/plugins/security/server/audit/security_audit_logger.ts index 87f7201f85665..ee81f5f330f44 100644 --- a/x-pack/plugins/security/server/audit/security_audit_logger.ts +++ b/x-pack/plugins/security/server/audit/security_audit_logger.ts @@ -5,11 +5,17 @@ */ import { AuthenticationProvider } from '../../common/types'; -import { AuditLogger } from './audit_service'; +import { LegacyAuditLogger } from './audit_service'; +/** + * @deprecated + */ export class SecurityAuditLogger { - constructor(private readonly logger: AuditLogger) {} + constructor(private readonly logger: LegacyAuditLogger) {} + /** + * @deprecated + */ savedObjectsAuthorizationFailure( username: string, action: string, @@ -37,6 +43,9 @@ export class SecurityAuditLogger { ); } + /** + * @deprecated + */ savedObjectsAuthorizationSuccess( username: string, action: string, @@ -59,6 +68,9 @@ export class SecurityAuditLogger { ); } + /** + * @deprecated + */ accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) { this.logger.log( 'access_agreement_acknowledged', diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 4f52ebe3065a3..e5bb00cdc056f 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -19,7 +19,7 @@ import { } from '../../../../../src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; -import { securityAuditLoggerMock } from '../audit/index.mock'; +import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock'; import { sessionMock } from '../session_management/index.mock'; import { SecurityLicenseFeatures } from '../../common/licensing'; import { ConfigSchema, createConfig } from '../config'; @@ -40,7 +40,8 @@ function getMockOptions({ selector?: AuthenticatorOptions['config']['authc']['selector']; } = {}) { return { - auditLogger: securityAuditLoggerMock.create(), + legacyAuditLogger: securityAuditLoggerMock.create(), + audit: auditServiceMock.create(), getCurrentUser: jest.fn(), clusterClient: elasticsearchServiceMock.createLegacyClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, @@ -215,9 +216,15 @@ describe('Authenticator', () => { let authenticator: Authenticator; let mockOptions: ReturnType; let mockSessVal: SessionValue; + const auditLogger = { + log: jest.fn(), + }; + beforeEach(() => { + auditLogger.log.mockClear(); mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); mockOptions.session.get.mockResolvedValue(null); + mockOptions.audit.asScoped.mockReturnValue(auditLogger); mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } }); authenticator = new Authenticator(mockOptions); @@ -280,6 +287,49 @@ describe('Authenticator', () => { ); }); + it('adds audit event when successful.', async () => { + const request = httpServerMock.createKibanaRequest(); + const user = mockAuthenticatedUser(); + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } }) + ); + await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: { action: 'user_login', category: 'authentication', outcome: 'success' }, + }) + ); + }); + + it('adds audit event when not successful.', async () => { + const request = httpServerMock.createKibanaRequest(); + const failureReason = new Error('Not Authorized'); + mockBasicAuthenticationProvider.login.mockResolvedValue( + AuthenticationResult.failed(failureReason) + ); + await authenticator.login(request, { provider: { type: 'basic' }, value: {} }); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: { action: 'user_login', category: 'authentication', outcome: 'failure' }, + }) + ); + }); + + it('does not add audit event when not handled.', async () => { + const request = httpServerMock.createKibanaRequest(); + await expect( + authenticator.login(request, { provider: { type: 'token' }, value: {} }) + ).resolves.toEqual(AuthenticationResult.notHandled()); + + await authenticator.login(request, { provider: { name: 'basic2' }, value: {} }); + + expect(auditLogger.log).not.toHaveBeenCalled(); + }); + it('creates session whenever authentication provider returns state', async () => { const user = mockAuthenticatedUser(); const request = httpServerMock.createKibanaRequest(); @@ -1859,11 +1909,14 @@ describe('Authenticator', () => { accessAgreementAcknowledged: true, }); - expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1); - expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith('user', { - type: 'basic', - name: 'basic1', - }); + expect(mockOptions.legacyAuditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1); + expect(mockOptions.legacyAuditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith( + 'user', + { + type: 'basic', + name: 'basic1', + } + ); expect( mockOptions.getFeatureUsageService().recordPreAccessAgreementUsage diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index b8ec6258eb0d5..0523ebaffb9d0 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -13,7 +13,7 @@ import { import { SecurityLicense } from '../../common/licensing'; import { AuthenticatedUser } from '../../common/model'; import { AuthenticationProvider } from '../../common/types'; -import { SecurityAuditLogger } from '../audit'; +import { SecurityAuditLogger, AuditServiceSetup, userLoginEvent } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { SecurityFeatureUsageServiceStart } from '../feature_usage'; @@ -59,7 +59,8 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick; @@ -293,6 +294,20 @@ export class Authenticator { existingSessionValue, }); + // Checking for presence of `user` object to determine success state rather than + // `success()` method since that indicates a successful authentication and `redirect()` + // could also (but does not always) authenticate a user successfully (e.g. SAML flow) + if (authenticationResult.user || authenticationResult.failed()) { + const auditLogger = this.options.audit.asScoped(request); + auditLogger.log( + userLoginEvent({ + authenticationResult, + authenticationProvider: providerName, + authenticationType: provider.type, + }) + ); + } + return this.handlePreAccessRedirects( request, authenticationResult, @@ -421,7 +436,7 @@ export class Authenticator { accessAgreementAcknowledged: true, }); - this.options.auditLogger.accessAgreementAcknowledged( + this.options.legacyAuditLogger.accessAgreementAcknowledged( currentUser.username, existingSessionValue.provider ); diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 263ea5c4e5041..6f8f17a0a3c74 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -18,7 +18,7 @@ import { } from '../../../../../src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; -import { securityAuditLoggerMock } from '../audit/index.mock'; +import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock'; import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock'; import { sessionMock } from '../session_management/session.mock'; @@ -42,13 +42,14 @@ import { InvalidateAPIKeyParams, } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; -import { SecurityAuditLogger } from '../audit'; +import { AuditServiceSetup, SecurityAuditLogger } from '../audit'; import { SecurityFeatureUsageServiceStart } from '../feature_usage'; import { Session } from '../session_management'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { - auditLogger: jest.Mocked; + legacyAuditLogger: jest.Mocked; + audit: jest.Mocked; config: ConfigType; loggers: LoggerFactory; http: jest.Mocked; @@ -60,7 +61,8 @@ describe('setupAuthentication()', () => { let mockScopedClusterClient: jest.Mocked>; beforeEach(() => { mockSetupAuthenticationParams = { - auditLogger: securityAuditLoggerMock.create(), + legacyAuditLogger: securityAuditLoggerMock.create(), + audit: auditServiceMock.create(), http: coreMock.createSetup().http, config: createConfig( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 431c82fb28a6c..ab8e42a6a72da 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -12,7 +12,7 @@ import { } from '../../../../../src/core/server'; import { SecurityLicense } from '../../common/licensing'; import { AuthenticatedUser } from '../../common/model'; -import { SecurityAuditLogger } from '../audit'; +import { SecurityAuditLogger, AuditServiceSetup } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { SecurityFeatureUsageServiceStart } from '../feature_usage'; @@ -45,7 +45,8 @@ export { } from './http_authentication'; interface SetupAuthenticationParams { - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; http: HttpServiceSetup; clusterClient: ILegacyClusterClient; @@ -58,7 +59,8 @@ interface SetupAuthenticationParams { export type Authentication = UnwrapPromise>; export async function setupAuthentication({ - auditLogger, + legacyAuditLogger: auditLogger, + audit, getFeatureUsageService, http, clusterClient, @@ -82,7 +84,8 @@ export async function setupAuthentication({ }; const authenticator = new Authenticator({ - auditLogger, + legacyAuditLogger: auditLogger, + audit, loggers, clusterClient, basePath: http.basePath, diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 093a7643fbf64..32b8708d2b381 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('crypto', () => ({ randomBytes: jest.fn() })); +jest.mock('crypto', () => ({ + randomBytes: jest.fn(), + constants: jest.requireActual('crypto').constants, +})); import { loggingSystemMock } from '../../../../src/core/server/mocks'; import { createConfig, ConfigSchema } from './config'; @@ -150,31 +153,23 @@ describe('config schema', () => { }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { - expect(() => - ConfigSchema.validate({ encryptionKey: 'foo' }) - ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` + expect(() => ConfigSchema.validate({ encryptionKey: 'foo' })).toThrow( + '[encryptionKey]: value has length [3] but it must have a minimum length of [32].' ); - expect(() => - ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true }) - ).toThrowErrorMatchingInlineSnapshot( - `"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."` + expect(() => ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true })).toThrow( + '[encryptionKey]: value has length [3] but it must have a minimum length of [32].' ); }); describe('authc.oidc', () => { it(`returns a validation error when authc.providers is "['oidc']" and realm is unspecified`, async () => { - expect(() => - ConfigSchema.validate({ authc: { providers: ['oidc'] } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.oidc.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'] } })).toThrow( + '[authc.oidc.realm]: expected value of type [string] but got [undefined]' ); - expect(() => - ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.oidc.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['oidc'], oidc: {} } })).toThrow( + '[authc.oidc.realm]: expected value of type [string] but got [undefined]' ); }); @@ -204,10 +199,8 @@ describe('config schema', () => { }); it(`returns a validation error when authc.providers is "['oidc', 'basic']" and realm is unspecified`, async () => { - expect(() => - ConfigSchema.validate({ authc: { providers: ['oidc', 'basic'] } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.oidc.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['oidc', 'basic'] } })).toThrow( + '[authc.oidc.realm]: expected value of type [string] but got [undefined]' ); }); @@ -240,22 +233,18 @@ describe('config schema', () => { it(`realm is not allowed when authc.providers is "['basic']"`, async () => { expect(() => ConfigSchema.validate({ authc: { providers: ['basic'], oidc: { realm: 'realm-1' } } }) - ).toThrowErrorMatchingInlineSnapshot(`"[authc.oidc]: a value wasn't expected to be present"`); + ).toThrow("[authc.oidc]: a value wasn't expected to be present"); }); }); describe('authc.saml', () => { it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => { - expect(() => - ConfigSchema.validate({ authc: { providers: ['saml'] } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.saml.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['saml'] } })).toThrow( + '[authc.saml.realm]: expected value of type [string] but got [undefined]' ); - expect(() => - ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[authc.saml.realm]: expected value of type [string] but got [undefined]"` + expect(() => ConfigSchema.validate({ authc: { providers: ['saml'], saml: {} } })).toThrow( + '[authc.saml.realm]: expected value of type [string] but got [undefined]' ); expect( @@ -285,7 +274,7 @@ describe('config schema', () => { it('`realm` is not allowed if saml provider is not enabled', async () => { expect(() => ConfigSchema.validate({ authc: { providers: ['basic'], saml: { realm: 'realm-1' } } }) - ).toThrowErrorMatchingInlineSnapshot(`"[authc.saml]: a value wasn't expected to be present"`); + ).toThrow("[authc.saml]: a value wasn't expected to be present"); }); it('`maxRedirectURLSize` accepts any positive value that can coerce to `ByteSizeValue`', async () => { @@ -360,11 +349,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { basic: { basic1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic.basic1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.basic.basic1.order]: expected value of type [number] but got [undefined]' + ); }); it('cannot be hidden from selector', () => { @@ -374,11 +361,9 @@ describe('config schema', () => { providers: { basic: { basic1: { order: 0, showInSelector: false } } }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic.basic1.showInSelector]: \`basic\` provider only supports \`true\` in \`showInSelector\`." -`); + ).toThrow( + '[authc.providers.1.basic.basic1.showInSelector]: `basic` provider only supports `true` in `showInSelector`.' + ); }); it('can have only provider of this type', () => { @@ -386,11 +371,7 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { basic: { basic1: { order: 0 }, basic2: { order: 1 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic]: Only one \\"basic\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.basic]: Only one "basic" provider can be configured'); }); it('can be successfully validated', () => { @@ -420,11 +401,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { token: { token1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token.token1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.token.token1.order]: expected value of type [number] but got [undefined]' + ); }); it('cannot be hidden from selector', () => { @@ -434,11 +413,9 @@ describe('config schema', () => { providers: { token: { token1: { order: 0, showInSelector: false } } }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token.token1.showInSelector]: \`token\` provider only supports \`true\` in \`showInSelector\`." -`); + ).toThrow( + '[authc.providers.1.token.token1.showInSelector]: `token` provider only supports `true` in `showInSelector`.' + ); }); it('can have only provider of this type', () => { @@ -446,11 +423,7 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { token: { token1: { order: 0 }, token2: { order: 1 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token]: Only one \\"token\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.token]: Only one "token" provider can be configured'); }); it('can be successfully validated', () => { @@ -480,11 +453,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { pki: { pki1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.pki.pki1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.pki.pki1.order]: expected value of type [number] but got [undefined]' + ); }); it('can have only provider of this type', () => { @@ -492,11 +463,7 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { pki: { pki1: { order: 0 }, pki2: { order: 1 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.pki]: Only one \\"pki\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.pki]: Only one "pki" provider can be configured'); }); it('can be successfully validated', () => { @@ -524,11 +491,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { kerberos: { kerberos1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.kerberos.kerberos1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.kerberos.kerberos1.order]: expected value of type [number] but got [undefined]' + ); }); it('can have only provider of this type', () => { @@ -538,11 +503,7 @@ describe('config schema', () => { providers: { kerberos: { kerberos1: { order: 0 }, kerberos2: { order: 1 } } }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.kerberos]: Only one \\"kerberos\\" provider can be configured." -`); + ).toThrow('[authc.providers.1.kerberos]: Only one "kerberos" provider can be configured'); }); it('can be successfully validated', () => { @@ -570,11 +531,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { oidc: { oidc1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.oidc.oidc1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.oidc.oidc1.order]: expected value of type [number] but got [undefined]' + ); }); it('requires `realm`', () => { @@ -582,11 +541,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { oidc: { oidc1: { order: 0 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.oidc.oidc1.realm]: expected value of type [string] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.oidc.oidc1.realm]: expected value of type [string] but got [undefined]' + ); }); it('can be successfully validated', () => { @@ -625,11 +582,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { saml: { saml1: { enabled: true } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.saml.saml1.order]: expected value of type [number] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.saml.saml1.order]: expected value of type [number] but got [undefined]' + ); }); it('requires `realm`', () => { @@ -637,11 +592,9 @@ describe('config schema', () => { ConfigSchema.validate({ authc: { providers: { saml: { saml1: { order: 0 } } } }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.saml.saml1.realm]: expected value of type [string] but got [undefined]" -`); + ).toThrow( + '[authc.providers.1.saml.saml1.realm]: expected value of type [string] but got [undefined]' + ); }); it('can be successfully validated', () => { @@ -703,11 +656,9 @@ describe('config schema', () => { }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1]: Found multiple providers configured with the same name \\"provider1\\": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider1]" -`); + ).toThrow( + '[authc.providers.1]: Found multiple providers configured with the same name "provider1": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider1]' + ); }); it('`order` should be unique across all provider types', () => { @@ -723,11 +674,9 @@ describe('config schema', () => { }, }, }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1]: Found multiple providers configured with the same order \\"0\\": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider2]" -`); + ).toThrow( + '[authc.providers.1]: Found multiple providers configured with the same order "0": [xpack.security.authc.providers.basic.provider1, xpack.security.authc.providers.saml.provider2]' + ); }); it('can be successfully validated with multiple providers ignoring uniqueness violations in disabled ones', () => { @@ -792,10 +741,8 @@ describe('config schema', () => { describe('session', () => { it('should throw error if xpack.security.session.cleanupInterval is less than 10 seconds', () => { - expect(() => - ConfigSchema.validate({ session: { cleanupInterval: '9s' } }) - ).toThrowErrorMatchingInlineSnapshot( - `"[session.cleanupInterval]: the value must be greater or equal to 10 seconds."` + expect(() => ConfigSchema.validate({ session: { cleanupInterval: '9s' } })).toThrow( + '[session.cleanupInterval]: the value must be greater or equal to 10 seconds.' ); }); }); @@ -1091,4 +1038,55 @@ describe('createConfig()', () => { ] `); }); + + it('accepts an audit appender', () => { + expect( + ConfigSchema.validate({ + audit: { + appender: { + kind: 'file', + path: '/path/to/file.txt', + layout: { + kind: 'json', + }, + }, + }, + }).audit.appender + ).toMatchInlineSnapshot(` + Object { + "kind": "file", + "layout": Object { + "kind": "json", + }, + "path": "/path/to/file.txt", + } + `); + }); + + it('rejects an appender if not fully configured', () => { + expect(() => + ConfigSchema.validate({ + audit: { + // no layout configured + appender: { + kind: 'file', + path: '/path/to/file.txt', + }, + }, + }) + ).toThrow('[audit.appender.2.kind]: expected value to equal [legacy-appender]'); + }); + + it('rejects an ignore_filter when no appender is configured', () => { + expect(() => + ConfigSchema.validate({ + audit: { + enabled: true, + ignore_filters: [{ actions: ['some_action'] }], + }, + }) + ).toThrow( + '[audit]: xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.' + ); + }); }); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 9ccbdac5e09f4..80b46a67ce011 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -7,7 +7,7 @@ import crypto from 'crypto'; import { schema, Type, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { Logger } from '../../../../src/core/server'; +import { Logger, config as coreConfig } from '../../../../src/core/server'; export type ConfigType = ReturnType; @@ -198,9 +198,30 @@ export const ConfigSchema = schema.object({ schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }), }), }), - audit: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - }), + audit: schema.object( + { + enabled: schema.boolean({ defaultValue: false }), + appender: schema.maybe(coreConfig.logging.appenders), + ignore_filters: schema.maybe( + schema.arrayOf( + schema.object({ + actions: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + categories: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + types: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + outcomes: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + spaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + }) + ) + ), + }, + { + validate: (auditConfig) => { + if (auditConfig.ignore_filters && !auditConfig.appender) { + return 'xpack.security.audit.ignore_filters can only be used with the ECS audit logger. To enable the ECS audit logger, specify where you want to write the audit events using xpack.security.audit.appender.'; + } + }, + } + ), }); export function createConfig( diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 00ad962115901..04db65f88cda0 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -27,7 +27,7 @@ export { SAMLLogin, OIDCLogin, } from './authentication'; -export { AuditLogger } from './audit'; +export { LegacyAuditLogger } from './audit'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 9088d4f08d0ef..9b08ba8c275fd 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -55,6 +55,7 @@ describe('Security Plugin', () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { "audit": Object { + "asScoped": [Function], "getLogger": [Function], }, "authc": Object { diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 5edc4c2357277..52283290ba7b7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -67,7 +67,7 @@ export interface SecurityPluginSetup { 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' >; license: SecurityLicense; - audit: Pick; + audit: AuditServiceSetup; /** * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin @@ -101,6 +101,7 @@ export class Plugin { private readonly logger: Logger; private spacesService?: SpacesService | symbol = Symbol('not accessed'); private securityLicenseService?: SecurityLicenseService; + private authc?: Authentication; private readonly featureUsageService = new SecurityFeatureUsageService(); private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; @@ -176,8 +177,15 @@ export class Plugin { registerSecurityUsageCollector({ usageCollection, config, license }); - const audit = this.auditService.setup({ license, config: config.audit }); - const auditLogger = new SecurityAuditLogger(audit.getLogger()); + const audit = this.auditService.setup({ + license, + config: config.audit, + logging: core.logging, + http: core.http, + getSpaceId: (request) => this.getSpacesService()?.getSpaceId(request), + getCurrentUser: (request) => this.authc?.getCurrentUser(request), + }); + const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger()); const { session } = this.sessionManagementService.setup({ config, @@ -187,8 +195,9 @@ export class Plugin { taskManager, }); - const authc = await setupAuthentication({ - auditLogger, + this.authc = await setupAuthentication({ + legacyAuditLogger, + audit, getFeatureUsageService: this.getFeatureUsageService, http: core.http, clusterClient, @@ -209,11 +218,12 @@ export class Plugin { buildNumber: this.initializerContext.env.packageInfo.buildNum, getSpacesService: this.getSpacesService, features, - getCurrentUser: authc.getCurrentUser, + getCurrentUser: this.authc.getCurrentUser, }); setupSavedObjects({ - auditLogger, + legacyAuditLogger, + audit, authz, savedObjects: core.savedObjects, getSpacesService: this.getSpacesService, @@ -226,7 +236,7 @@ export class Plugin { logger: this.initializerContext.logger.get('routes'), clusterClient, config, - authc, + authc: this.authc, authz, license, session, @@ -239,17 +249,18 @@ export class Plugin { return deepFreeze({ audit: { + asScoped: audit.asScoped, getLogger: audit.getLogger, }, authc: { - isAuthenticated: authc.isAuthenticated, - getCurrentUser: authc.getCurrentUser, - areAPIKeysEnabled: authc.areAPIKeysEnabled, - createAPIKey: authc.createAPIKey, - invalidateAPIKey: authc.invalidateAPIKey, - grantAPIKeyAsInternalUser: authc.grantAPIKeyAsInternalUser, - invalidateAPIKeyAsInternalUser: authc.invalidateAPIKeyAsInternalUser, + isAuthenticated: this.authc.isAuthenticated, + getCurrentUser: this.authc.getCurrentUser, + areAPIKeysEnabled: this.authc.areAPIKeysEnabled, + createAPIKey: this.authc.createAPIKey, + invalidateAPIKey: this.authc.invalidateAPIKey, + grantAPIKeyAsInternalUser: this.authc.grantAPIKeyAsInternalUser, + invalidateAPIKeyAsInternalUser: this.authc.invalidateAPIKeyAsInternalUser, }, authz: { diff --git a/x-pack/plugins/security/server/saved_objects/index.ts b/x-pack/plugins/security/server/saved_objects/index.ts index 6acfd06a0309b..16c935e048930 100644 --- a/x-pack/plugins/security/server/saved_objects/index.ts +++ b/x-pack/plugins/security/server/saved_objects/index.ts @@ -12,11 +12,12 @@ import { } from '../../../../../src/core/server'; import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; import { AuthorizationServiceSetup } from '../authorization'; -import { SecurityAuditLogger } from '../audit'; +import { SecurityAuditLogger, AuditServiceSetup } from '../audit'; import { SpacesService } from '../plugin'; interface SetupSavedObjectsParams { - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + audit: AuditServiceSetup; authz: Pick< AuthorizationServiceSetup, 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest' @@ -26,7 +27,8 @@ interface SetupSavedObjectsParams { } export function setupSavedObjects({ - auditLogger, + legacyAuditLogger, + audit, authz, savedObjects, getSpacesService, @@ -50,7 +52,8 @@ export function setupSavedObjects({ return authz.mode.useRbacForRequest(kibanaRequest) ? new SecureSavedObjectsClientWrapper({ actions: authz.actions, - auditLogger, + legacyAuditLogger, + auditLogger: audit.asScoped(kibanaRequest), baseClient: client, checkSavedObjectsPrivilegesAsCurrentUser: authz.checkSavedObjectsPrivilegesWithRequest( kibanaRequest diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index af1aaf16f7fed..8136553e4a623 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -6,10 +6,11 @@ import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper'; import { Actions } from '../authorization'; -import { securityAuditLoggerMock } from '../audit/index.mock'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { securityAuditLoggerMock, auditServiceMock } from '../audit/index.mock'; +import { savedObjectsClientMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { SavedObjectsClientContract } from 'kibana/server'; import { SavedObjectActions } from '../authorization/actions/saved_object'; +import { AuditEvent, EventOutcome } from '../audit'; let clientOpts: ReturnType; let client: SecureSavedObjectsClientWrapper; @@ -38,7 +39,8 @@ const createSecureSavedObjectsClientWrapperOptions = () => { checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(), errors, getSpacesService, - auditLogger: securityAuditLoggerMock.create(), + legacyAuditLogger: securityAuditLoggerMock.create(), + auditLogger: auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()), forbiddenError, generalError, }; @@ -53,8 +55,8 @@ const expectGeneralError = async (fn: Function, args: Record) => { clientOpts.generalError ); expect(clientOpts.errors.decorateGeneralError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }; /** @@ -84,8 +86,8 @@ const expectForbiddenError = async (fn: Function, args: Record, act const spaceIds = [spaceId]; expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( USERNAME, action ?? ACTION, types, @@ -93,7 +95,7 @@ const expectForbiddenError = async (fn: Function, args: Record, act missing, args ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }; const expectSuccess = async (fn: Function, args: Record, action?: string) => { @@ -105,9 +107,9 @@ const expectSuccess = async (fn: Function, args: Record, action?: s const types = getCalls.map((x) => x[0]); const spaceIds = args.options?.namespaces || [args.options?.namespace || 'default']; - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( USERNAME, action ?? ACTION, types, @@ -176,6 +178,26 @@ const expectObjectNamespaceFiltering = async ( ); }; +const expectAuditEvent = ( + action: AuditEvent['event']['action'], + outcome: AuditEvent['event']['outcome'], + savedObject?: Required['kibana']['saved_object'] +) => { + expect(clientOpts.auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + action, + outcome, + }), + kibana: savedObject + ? expect.objectContaining({ + saved_object: savedObject, + }) + : expect.anything(), + }) + ); +}; + const expectObjectsNamespaceFiltering = async (fn: Function, args: Record) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementationOnce( getMockCheckPrivilegesSuccess // privilege check for authorization @@ -200,15 +222,13 @@ const expectObjectsNamespaceFiltering = async (fn: Function, args: Record { ); expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect( + clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure + ).toHaveBeenCalledWith( USERNAME, 'addToNamespacesCreate', [type], @@ -308,7 +330,7 @@ describe('#addToNamespaces', () => { [{ privilege, spaceId: newNs1 }], { id, type, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); test(`throws decorated ForbiddenError when unauthorized to update in current space`, async () => { @@ -324,9 +346,9 @@ describe('#addToNamespaces', () => { ); expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); expect( - clientOpts.auditLogger.savedObjectsAuthorizationFailure + clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure ).toHaveBeenLastCalledWith( USERNAME, 'addToNamespacesUpdate', @@ -335,7 +357,7 @@ describe('#addToNamespaces', () => { [{ privilege, spaceId: currentNs }], { id, type, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); }); test(`returns result of baseClient.addToNamespaces when authorized`, async () => { @@ -345,9 +367,9 @@ describe('#addToNamespaces', () => { const result = await client.addToNamespaces(type, id, namespaces); expect(result).toBe(apiCallReturnValue); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(2); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( 1, USERNAME, 'addToNamespacesCreate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesCreate' @@ -355,7 +377,7 @@ describe('#addToNamespaces', () => { namespaces.sort(), { type, id, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenNthCalledWith( 2, USERNAME, 'addToNamespacesUpdate', // action for privilege check is 'share_to_space', but auditAction is 'addToNamespacesUpdate' @@ -392,12 +414,28 @@ describe('#addToNamespaces', () => { // this operation is unique because it requires two privilege checks before it executes await expectObjectNamespaceFiltering(client.addToNamespaces, { type, id, namespaces }, 2); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.addToNamespaces.mockReturnValue(apiCallReturnValue as any); + await client.addToNamespaces(type, id, namespaces); + + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_add_to_spaces', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.addToNamespaces(type, id, namespaces)).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_add_to_spaces', EventOutcome.FAILURE, { type, id }); + }); }); describe('#bulkCreate', () => { const attributes = { some: 'attr' }; - const obj1 = Object.freeze({ type: 'foo', otherThing: 'sup', attributes }); - const obj2 = Object.freeze({ type: 'bar', otherThing: 'everyone', attributes }); + const obj1 = Object.freeze({ type: 'foo', id: 'sup', attributes }); + const obj2 = Object.freeze({ type: 'bar', id: 'everyone', attributes }); const namespace = 'some-ns'; test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { @@ -445,6 +483,25 @@ describe('#bulkCreate', () => { const options = { namespace }; await expectObjectsNamespaceFiltering(client.bulkCreate, { objects, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkCreate.mockReturnValue(apiCallReturnValue as any); + const objects = [obj1, obj2]; + const options = { namespace }; + await expectSuccess(client.bulkCreate, { objects, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type: obj2.type, id: obj2.id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.bulkCreate([obj1, obj2], { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type: obj2.type, id: obj2.id }); + }); }); describe('#bulkGet', () => { @@ -484,6 +541,25 @@ describe('#bulkGet', () => { const options = { namespace }; await expectObjectsNamespaceFiltering(client.bulkGet, { objects, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkGet.mockReturnValue(apiCallReturnValue as any); + const objects = [obj1, obj2]; + const options = { namespace }; + await expectSuccess(client.bulkGet, { objects, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, obj1); + expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, obj2); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.bulkGet([obj1, obj2], { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_get', EventOutcome.FAILURE, obj1); + expectAuditEvent('saved_object_get', EventOutcome.FAILURE, obj2); + }); }); describe('#bulkUpdate', () => { @@ -534,6 +610,25 @@ describe('#bulkUpdate', () => { const options = { namespace }; await expectObjectsNamespaceFiltering(client.bulkUpdate, { objects, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.bulkUpdate.mockReturnValue(apiCallReturnValue as any); + const objects = [obj1, obj2]; + const options = { namespace }; + await expectSuccess(client.bulkUpdate, { objects, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type: obj2.type, id: obj2.id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.bulkUpdate([obj1, obj2], { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type: obj1.type, id: obj1.id }); + expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type: obj2.type, id: obj2.id }); + }); }); describe('#checkConflicts', () => { @@ -614,6 +709,22 @@ describe('#create', () => { const options = { namespace }; await expectObjectNamespaceFiltering(client.create, { type, attributes, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.create.mockResolvedValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.create, { type, attributes, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_create', EventOutcome.UNKNOWN, { type }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.create(type, attributes, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_create', EventOutcome.FAILURE, { type }); + }); }); describe('#delete', () => { @@ -643,6 +754,22 @@ describe('#delete', () => { const options = { namespace }; await expectPrivilegeCheck(client.delete, { type, id, options }, namespace); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.delete.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.delete, { type, id, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.delete(type, id)).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete', EventOutcome.FAILURE, { type, id }); + }); }); describe('#find', () => { @@ -663,8 +790,10 @@ describe('#find', () => { const result = await client.find(options); expect(clientOpts.baseClient.find).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect( + clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure + ).toHaveBeenCalledWith( USERNAME, 'find', [type1], @@ -759,6 +888,27 @@ describe('#find', () => { const options = { type: [type1, type2], namespaces }; await expectObjectsNamespaceFiltering(client.find, { options }); }); + + test(`adds audit event when successful`, async () => { + const obj1 = { type: 'foo', id: 'sup' }; + const obj2 = { type: 'bar', id: 'everyone' }; + const apiCallReturnValue = { saved_objects: [obj1, obj2], foo: 'bar' }; + clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any); + const options = Object.freeze({ type: type1, namespaces: ['some-ns'] }); + await expectSuccess(client.find, { options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(2); + expectAuditEvent('saved_object_find', EventOutcome.SUCCESS, obj1); + expectAuditEvent('saved_object_find', EventOutcome.SUCCESS, obj2); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + await client.find({ type: type1 }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_find', EventOutcome.FAILURE); + }); }); describe('#get', () => { @@ -793,6 +943,22 @@ describe('#get', () => { const options = { namespace }; await expectObjectNamespaceFiltering(client.get, { type, id, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.get.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.get, { type, id, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_get', EventOutcome.SUCCESS, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.get(type, id, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_get', EventOutcome.FAILURE, { type, id }); + }); }); describe('#deleteFromNamespaces', () => { @@ -817,8 +983,8 @@ describe('#deleteFromNamespaces', () => { ); expect(clientOpts.errors.decorateForbiddenError).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( USERNAME, 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' [type], @@ -826,7 +992,7 @@ describe('#deleteFromNamespaces', () => { [{ privilege, spaceId: namespace1 }], { type, id, namespaces, options: {} } ); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); test(`returns result of baseClient.deleteFromNamespaces when authorized`, async () => { @@ -836,9 +1002,9 @@ describe('#deleteFromNamespaces', () => { const result = await client.deleteFromNamespaces(type, id, namespaces); expect(result).toBe(apiCallReturnValue); - expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(clientOpts.auditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(clientOpts.legacyAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith( USERNAME, 'deleteFromNamespaces', // action for privilege check is 'share_to_space', but auditAction is 'deleteFromNamespaces' [type], @@ -864,6 +1030,21 @@ describe('#deleteFromNamespaces', () => { test(`filters namespaces that the user doesn't have access to`, async () => { await expectObjectNamespaceFiltering(client.deleteFromNamespaces, { type, id, namespaces }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.deleteFromNamespaces.mockReturnValue(apiCallReturnValue as any); + await client.deleteFromNamespaces(type, id, namespaces); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete_from_spaces', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.deleteFromNamespaces(type, id, namespaces)).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_delete_from_spaces', EventOutcome.FAILURE, { type, id }); + }); }); describe('#update', () => { @@ -899,6 +1080,22 @@ describe('#update', () => { const options = { namespace }; await expectObjectNamespaceFiltering(client.update, { type, id, attributes, options }); }); + + test(`adds audit event when successful`, async () => { + const apiCallReturnValue = Symbol(); + clientOpts.baseClient.update.mockReturnValue(apiCallReturnValue as any); + const options = { namespace }; + await expectSuccess(client.update, { type, id, attributes, options }); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_update', EventOutcome.UNKNOWN, { type, id }); + }); + + test(`adds audit event when not successful`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockRejectedValue(new Error()); + await expect(() => client.update(type, id, attributes, { namespace })).rejects.toThrow(); + expect(clientOpts.auditLogger.log).toHaveBeenCalledTimes(1); + expectAuditEvent('saved_object_update', EventOutcome.FAILURE, { type, id }); + }); }); describe('other', () => { diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index d94dac942845e..c7a3f31cc517e 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -23,10 +23,12 @@ import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; import { CheckPrivilegesResponse } from '../authorization/types'; import { SpacesService } from '../plugin'; +import { AuditLogger, EventOutcome, SavedObjectAction, savedObjectEvent } from '../audit'; interface SecureSavedObjectsClientWrapperOptions { actions: Actions; - auditLogger: SecurityAuditLogger; + legacyAuditLogger: SecurityAuditLogger; + auditLogger: AuditLogger; baseClient: SavedObjectsClientContract; errors: SavedObjectsClientContract['errors']; checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; @@ -58,7 +60,8 @@ interface EnsureAuthorizedTypeResult { export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { private readonly actions: Actions; - private readonly auditLogger: PublicMethodsOf; + private readonly legacyAuditLogger: PublicMethodsOf; + private readonly auditLogger: AuditLogger; private readonly baseClient: SavedObjectsClientContract; private readonly checkSavedObjectsPrivilegesAsCurrentUser: CheckSavedObjectsPrivileges; private getSpacesService: () => SpacesService | undefined; @@ -66,6 +69,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra constructor({ actions, + legacyAuditLogger, auditLogger, baseClient, checkSavedObjectsPrivilegesAsCurrentUser, @@ -74,6 +78,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra }: SecureSavedObjectsClientWrapperOptions) { this.errors = errors; this.actions = actions; + this.legacyAuditLogger = legacyAuditLogger; this.auditLogger = auditLogger; this.baseClient = baseClient; this.checkSavedObjectsPrivilegesAsCurrentUser = checkSavedObjectsPrivilegesAsCurrentUser; @@ -85,9 +90,27 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: T = {} as T, options: SavedObjectsCreateOptions = {} ) { - const args = { type, attributes, options }; - const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; - await this.ensureAuthorized(type, 'create', namespaces, { args }); + try { + const args = { type, attributes, options }; + const namespaces = [options.namespace, ...(options.initialNamespaces || [])]; + await this.ensureAuthorized(type, 'create', namespaces, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type, id: options.id }, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id: options.id }, + }) + ); const savedObject = await this.baseClient.create(type, attributes, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -112,25 +135,65 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array>, options: SavedObjectsBaseOptions = {} ) { - const args = { objects, options }; - const namespaces = objects.reduce( - (acc, { initialNamespaces = [] }) => { - return acc.concat(initialNamespaces); - }, - [options.namespace] - ); + try { + const args = { objects, options }; + const namespaces = objects.reduce( + (acc, { initialNamespaces = [] }) => { + return acc.concat(initialNamespaces); + }, + [options.namespace] + ); - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, { - args, - }); + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_create', namespaces, { + args, + }); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.CREATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ) + ); const response = await this.baseClient.bulkCreate(objects, options); return await this.redactSavedObjectsNamespaces(response); } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { - const args = { type, id, options }; - await this.ensureAuthorized(type, 'delete', options.namespace, { args }); + try { + const args = { type, id, options }; + await this.ensureAuthorized(type, 'delete', options.namespace, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE, + savedObject: { type, id }, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ); return await this.baseClient.delete(type, id, options); } @@ -145,6 +208,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra `_find across namespaces is not permitted when the Spaces plugin is disabled.` ); } + const args = { options }; const { status, typeMap } = await this.ensureAuthorized( options.type, @@ -155,6 +219,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra if (status === 'unauthorized') { // return empty response + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.FIND, + error: new Error(status), + }) + ); return SavedObjectsUtils.createEmptyFindResponse(options); } @@ -163,11 +233,22 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra isGloballyAuthorized ? acc.set(type, options.namespaces) : acc.set(type, authorizedSpaces), new Map() ); + const response = await this.baseClient.find({ ...options, typeToNamespacesMap: undefined, // if the user is fully authorized, use `undefined` as the typeToNamespacesMap to prevent privilege escalation ...(status === 'partially_authorized' && { typeToNamespacesMap, type: '', namespaces: [] }), // the repository requires that `type` and `namespaces` must be empty if `typeToNamespacesMap` is defined }); + + response.saved_objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.FIND, + savedObject: { type, id }, + }) + ) + ); + return await this.redactSavedObjectsNamespaces(response); } @@ -175,20 +256,67 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ) { - const args = { objects, options }; - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, { - args, - }); + try { + const args = { objects, options }; + await this.ensureAuthorized( + this.getUniqueObjectTypes(objects), + 'bulk_get', + options.namespace, + { + args, + } + ); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } const response = await this.baseClient.bulkGet(objects, options); + + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + }) + ) + ); + return await this.redactSavedObjectsNamespaces(response); } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { - const args = { type, id, options }; - await this.ensureAuthorized(type, 'get', options.namespace, { args }); + try { + const args = { type, id, options }; + await this.ensureAuthorized(type, 'get', options.namespace, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + error, + }) + ); + throw error; + } const savedObject = await this.baseClient.get(type, id, options); + + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.GET, + savedObject: { type, id }, + }) + ); + return await this.redactSavedObjectNamespaces(savedObject); } @@ -198,8 +326,26 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: Partial, options: SavedObjectsUpdateOptions = {} ) { - const args = { type, id, attributes, options }; - await this.ensureAuthorized(type, 'update', options.namespace, { args }); + try { + const args = { type, id, attributes, options }; + await this.ensureAuthorized(type, 'update', options.namespace, { args }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + savedObject: { type, id }, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ); const savedObject = await this.baseClient.update(type, id, attributes, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -211,25 +357,45 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra namespaces: string[], options: SavedObjectsAddToNamespacesOptions = {} ) { - const args = { type, id, namespaces, options }; - const { namespace } = options; - // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'addToNamespacesCreate', - }); - - // To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the - // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in - // the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation - // will result in a 404 error. - await this.ensureAuthorized(type, 'share_to_space', namespace, { - args, - auditAction: 'addToNamespacesUpdate', - }); + try { + const args = { type, id, namespaces, options }; + const { namespace } = options; + // To share an object, the user must have the "share_to_space" permission in each of the destination namespaces. + await this.ensureAuthorized(type, 'share_to_space', namespaces, { + args, + auditAction: 'addToNamespacesCreate', + }); + + // To share an object, the user must also have the "share_to_space" permission in one or more of the source namespaces. Because the + // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "share_to_space" permission in + // the current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation + // will result in a 404 error. + await this.ensureAuthorized(type, 'share_to_space', namespace, { + args, + auditAction: 'addToNamespacesUpdate', + }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.ADD_TO_SPACES, + savedObject: { type, id }, + addToSpaces: namespaces, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.ADD_TO_SPACES, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + addToSpaces: namespaces, + }) + ); - const result = await this.baseClient.addToNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(result); + const response = await this.baseClient.addToNamespaces(type, id, namespaces, options); + return await this.redactSavedObjectNamespaces(response); } public async deleteFromNamespaces( @@ -238,31 +404,73 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra namespaces: string[], options: SavedObjectsDeleteFromNamespacesOptions = {} ) { - const args = { type, id, namespaces, options }; - // To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces. - await this.ensureAuthorized(type, 'share_to_space', namespaces, { - args, - auditAction: 'deleteFromNamespaces', - }); + try { + const args = { type, id, namespaces, options }; + // To un-share an object, the user must have the "share_to_space" permission in each of the target namespaces. + await this.ensureAuthorized(type, 'share_to_space', namespaces, { + args, + auditAction: 'deleteFromNamespaces', + }); + } catch (error) { + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE_FROM_SPACES, + savedObject: { type, id }, + deleteFromSpaces: namespaces, + error, + }) + ); + throw error; + } + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.DELETE_FROM_SPACES, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + deleteFromSpaces: namespaces, + }) + ); - const result = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); - return await this.redactSavedObjectNamespaces(result); + const response = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); + return await this.redactSavedObjectNamespaces(response); } public async bulkUpdate( objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { - const objectNamespaces = objects - // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; - // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. - .filter(({ namespace }) => namespace !== undefined) - .map(({ namespace }) => namespace!); - const namespaces = [options?.namespace, ...objectNamespaces]; - const args = { objects, options }; - await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { - args, - }); + try { + const objectNamespaces = objects + // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; + // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. + .filter(({ namespace }) => namespace !== undefined) + .map(({ namespace }) => namespace!); + const namespaces = [options?.namespace, ...objectNamespaces]; + const args = { objects, options }; + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { + args, + }); + } catch (error) { + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + savedObject: { type, id }, + error, + }) + ) + ); + throw error; + } + objects.forEach(({ type, id }) => + this.auditLogger.log( + savedObjectEvent({ + action: SavedObjectAction.UPDATE, + outcome: EventOutcome.UNKNOWN, + savedObject: { type, id }, + }) + ) + ); const response = await this.baseClient.bulkUpdate(objects, options); return await this.redactSavedObjectsNamespaces(response); @@ -316,7 +524,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); const logAuthorizationFailure = () => { - this.auditLogger.savedObjectsAuthorizationFailure( + this.legacyAuditLogger.savedObjectsAuthorizationFailure( username, auditAction, types, @@ -326,7 +534,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ); }; const logAuthorizationSuccess = (typeArray: string[], spaceIdArray: string[]) => { - this.auditLogger.savedObjectsAuthorizationSuccess( + this.legacyAuditLogger.savedObjectsAuthorizationSuccess( username, auditAction, typeArray, diff --git a/x-pack/plugins/spaces/server/lib/audit_logger.ts b/x-pack/plugins/spaces/server/lib/audit_logger.ts index da7c3886277cb..8110e3fbc6624 100644 --- a/x-pack/plugins/spaces/server/lib/audit_logger.ts +++ b/x-pack/plugins/spaces/server/lib/audit_logger.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditLogger } from '../../../security/server'; +import { LegacyAuditLogger } from '../../../security/server'; export class SpacesAuditLogger { - private readonly auditLogger: AuditLogger; + private readonly auditLogger: LegacyAuditLogger; - constructor(auditLogger: AuditLogger = { log() {} }) { + constructor(auditLogger: LegacyAuditLogger = { log() {} }) { this.auditLogger = auditLogger; } public spacesAuthorizationFailure(username: string, action: string, spaceIds?: string[]) { diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 6c0edd904b0e7..b15a2cf8d1f1d 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -35,6 +35,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/security_api_integration/session_idle.config.ts'), require.resolve('../test/security_api_integration/session_lifespan.config.ts'), require.resolve('../test/security_api_integration/login_selector.config.ts'), + require.resolve('../test/security_api_integration/audit.config.ts'), require.resolve('../test/token_api_integration/config.js'), require.resolve('../test/oidc_api_integration/config.ts'), require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'), diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 40a3b3cf1877f..e7d96023f3653 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -27,7 +27,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { // list paths to the files that contain your plugins tests testFiles: [ - resolve(__dirname, './test_suites/audit_trail'), resolve(__dirname, './test_suites/resolver'), resolve(__dirname, './test_suites/global_search'), ], @@ -50,12 +49,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { )}`, // Required to load new platform plugins via `--plugin-path` flag. '--env.name=development', - - '--xpack.audit_trail.enabled=true', - '--xpack.audit_trail.logger.enabled=true', - '--xpack.audit_trail.appender.kind=file', - '--xpack.audit_trail.appender.path=x-pack/test/plugin_functional/plugins/audit_trail_test/server/pattern_debug.log', - '--xpack.audit_trail.appender.layout.kind=json', ], }, uiSettings: xpackFunctionalConfig.get('uiSettings'), diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore deleted file mode 100644 index 9a3d281179193..0000000000000 --- a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/*debug.log diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts b/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts deleted file mode 100644 index 264f436fb1dc0..0000000000000 --- a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/plugin.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Plugin, CoreSetup } from 'src/core/server'; - -export class AuditTrailTestPlugin implements Plugin { - public setup(core: CoreSetup) { - core.savedObjects.registerType({ - name: 'audit_trail_test', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: {}, - }, - }); - - const router = core.http.createRouter(); - router.get( - { path: '/audit_trail_test/context/as_current_user', validate: false }, - async (context, request, response) => { - context.core.auditor.withAuditScope('audit_trail_test/context/as_current_user'); - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return response.noContent(); - } - ); - - router.get( - { path: '/audit_trail_test/context/as_internal_user', validate: false }, - async (context, request, response) => { - context.core.auditor.withAuditScope('audit_trail_test/context/as_internal_user'); - await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return response.noContent(); - } - ); - - router.get( - { path: '/audit_trail_test/contract/as_current_user', validate: false }, - async (context, request, response) => { - const [coreStart] = await core.getStartServices(); - const auditor = coreStart.auditTrail.asScoped(request); - auditor.withAuditScope('audit_trail_test/contract/as_current_user'); - - await context.core.elasticsearch.legacy.client.callAsCurrentUser('ping'); - return response.noContent(); - } - ); - - router.get( - { path: '/audit_trail_test/contract/as_internal_user', validate: false }, - async (context, request, response) => { - const [coreStart] = await core.getStartServices(); - const auditor = coreStart.auditTrail.asScoped(request); - auditor.withAuditScope('audit_trail_test/contract/as_internal_user'); - - await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return response.noContent(); - } - ); - } - - public start() {} -} diff --git a/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts b/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts deleted file mode 100644 index fb66f0dffc12a..0000000000000 --- a/x-pack/test/plugin_functional/test_suites/audit_trail/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Path from 'path'; -import Fs from 'fs'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -class FileWrapper { - constructor(private readonly path: string) {} - async reset() { - // "touch" each file to ensure it exists and is empty before each test - await Fs.promises.writeFile(this.path, ''); - } - async read() { - const content = await Fs.promises.readFile(this.path, { encoding: 'utf8' }); - return content.trim().split('\n'); - } - async readJSON() { - const content = await this.read(); - return content.map((l) => JSON.parse(l)); - } - // writing in a file is an async operation. we use this method to make sure logs have been written. - async isNotEmpty() { - const content = await this.read(); - const line = content[0]; - return line.length > 0; - } -} - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const retry = getService('retry'); - - describe('Audit trail service', function () { - this.tags('ciGroup7'); - const logFilePath = Path.resolve( - __dirname, - '../../plugins/audit_trail_test/server/pattern_debug.log' - ); - const logFile = new FileWrapper(logFilePath); - - beforeEach(async () => { - await logFile.reset(); - }); - - it('logs current user access to elasticsearch via RequestHandlerContext', async () => { - await supertest - .get('/audit_trail_test/context/as_current_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/context/as_current_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.currentUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - - it('logs internal user access to elasticsearch via RequestHandlerContext', async () => { - await supertest - .get('/audit_trail_test/context/as_internal_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/context/as_internal_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.internalUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - - it('logs current user access to elasticsearch via coreStart contract', async () => { - await supertest - .get('/audit_trail_test/contract/as_current_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/contract/as_current_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.currentUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - - it('logs internal user access to elasticsearch via coreStart contract', async () => { - await supertest - .get('/audit_trail_test/contract/as_internal_user') - .set('kbn-xsrf', 'foo') - .expect(204); - - await retry.waitFor('logs event in the dest file', async () => { - return await logFile.isNotEmpty(); - }); - - const content = await logFile.readJSON(); - const pingCall = content.find( - (c) => c.meta.scope === 'audit_trail_test/contract/as_internal_user' - ); - expect(pingCall).to.be.ok(); - expect(pingCall.meta.type).to.be('elasticsearch.call.internalUser'); - expect(pingCall.meta.user).to.be('elastic'); - expect(pingCall.meta.space).to.be('default'); - }); - }); -} diff --git a/x-pack/test/security_api_integration/audit.config.ts b/x-pack/test/security_api_integration/audit.config.ts new file mode 100644 index 0000000000000..c2011fafd1c9c --- /dev/null +++ b/x-pack/test/security_api_integration/audit.config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + const auditLogPlugin = resolve(__dirname, './fixtures/audit/audit_log'); + const auditLogPath = resolve(__dirname, './fixtures/audit/audit.log'); + + return { + testFiles: [require.resolve('./tests/audit')], + servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, + services: xPackAPITestsConfig.get('services'), + junit: { + reportName: 'X-Pack Security API Integration Tests (Audit Log)', + }, + esTestCluster: xPackAPITestsConfig.get('esTestCluster'), + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${auditLogPlugin}`, + '--xpack.security.audit.enabled=true', + '--xpack.security.audit.appender.kind=file', + `--xpack.security.audit.appender.path=${auditLogPath}`, + '--xpack.security.audit.appender.layout.kind=json', + ], + }, + }; +} diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json b/x-pack/test/security_api_integration/fixtures/audit/audit_log/kibana.json similarity index 62% rename from x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json rename to x-pack/test/security_api_integration/fixtures/audit/audit_log/kibana.json index f53aa57ad6705..fbec5108ee484 100644 --- a/x-pack/test/plugin_functional/plugins/audit_trail_test/kibana.json +++ b/x-pack/test/security_api_integration/fixtures/audit/audit_log/kibana.json @@ -1,9 +1,9 @@ { - "id": "audit_trail_test", + "id": "auditLog", "version": "1.0.0", "kibanaVersion": "kibana", "configPath": [], - "requiredPlugins": ["auditTrail"], + "requiredPlugins": [], "server": true, "ui": false } diff --git a/x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts b/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/index.ts similarity index 100% rename from x-pack/test/plugin_functional/plugins/audit_trail_test/server/index.ts rename to x-pack/test/security_api_integration/fixtures/audit/audit_log/server/index.ts diff --git a/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts b/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts new file mode 100644 index 0000000000000..9f594cd5889b6 --- /dev/null +++ b/x-pack/test/security_api_integration/fixtures/audit/audit_log/server/plugin.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup } from 'src/core/server'; + +export class AuditTrailTestPlugin implements Plugin { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + router.get({ path: '/audit_log', validate: false }, async (context, request, response) => { + await context.core.savedObjects.client.create('dashboard', {}); + await context.core.savedObjects.client.find({ type: 'dashboard' }); + return response.noContent(); + }); + } + + public start() {} +} diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts new file mode 100644 index 0000000000000..136854eab2866 --- /dev/null +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Path from 'path'; +import Fs from 'fs'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +class FileWrapper { + constructor(private readonly path: string) {} + async reset() { + // "touch" each file to ensure it exists and is empty before each test + await Fs.promises.writeFile(this.path, ''); + } + async read() { + const content = await Fs.promises.readFile(this.path, { encoding: 'utf8' }); + return content.trim().split('\n'); + } + async readJSON() { + const content = await this.read(); + return content.map((l) => JSON.parse(l)); + } + // writing in a file is an async operation. we use this method to make sure logs have been written. + async isNotEmpty() { + const content = await this.read(); + const line = content[0]; + return line.length > 0; + } +} + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + const { username, password } = getService('config').get('servers.kibana'); + + describe('Audit Log', function () { + const logFilePath = Path.resolve(__dirname, '../../fixtures/audit/audit.log'); + const logFile = new FileWrapper(logFilePath); + + beforeEach(async () => { + await logFile.reset(); + }); + + it('logs audit events when reading and writing saved objects', async () => { + await supertest.get('/audit_log?query=param').set('kbn-xsrf', 'foo').expect(204); + await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty()); + + const content = await logFile.readJSON(); + + const httpEvent = content.find((c) => c.event.action === 'http_request'); + expect(httpEvent).to.be.ok(); + expect(httpEvent.trace.id).to.be.ok(); + expect(httpEvent.user.name).to.be(username); + expect(httpEvent.kibana.space_id).to.be('default'); + expect(httpEvent.http.request.method).to.be('get'); + expect(httpEvent.url.path).to.be('/audit_log'); + expect(httpEvent.url.query).to.be('query=param'); + + const createEvent = content.find((c) => c.event.action === 'saved_object_create'); + expect(createEvent).to.be.ok(); + expect(createEvent.trace.id).to.be.ok(); + expect(createEvent.user.name).to.be(username); + expect(createEvent.kibana.space_id).to.be('default'); + + const findEvent = content.find((c) => c.event.action === 'saved_object_find'); + expect(findEvent).to.be.ok(); + expect(findEvent.trace.id).to.be.ok(); + expect(findEvent.user.name).to.be(username); + expect(findEvent.kibana.space_id).to.be('default'); + }); + + it('logs audit events when logging in successfully', async () => { + await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password }, + }) + .expect(200); + await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty()); + + const content = await logFile.readJSON(); + + const loginEvent = content.find((c) => c.event.action === 'user_login'); + expect(loginEvent).to.be.ok(); + expect(loginEvent.event.outcome).to.be('success'); + expect(loginEvent.trace.id).to.be.ok(); + expect(loginEvent.user.name).to.be(username); + }); + + it('logs audit events when failing to log in', async () => { + await supertest + .post('/internal/security/login') + .set('kbn-xsrf', 'xxx') + .send({ + providerType: 'basic', + providerName: 'basic', + currentURL: '/', + params: { username, password: 'invalid_password' }, + }) + .expect(401); + await retry.waitFor('logs event in the dest file', async () => await logFile.isNotEmpty()); + + const content = await logFile.readJSON(); + + const loginEvent = content.find((c) => c.event.action === 'user_login'); + expect(loginEvent).to.be.ok(); + expect(loginEvent.event.outcome).to.be('failure'); + expect(loginEvent.trace.id).to.be.ok(); + expect(loginEvent.user).not.to.be.ok(); + }); + }); +} diff --git a/x-pack/test/security_api_integration/tests/audit/index.ts b/x-pack/test/security_api_integration/tests/audit/index.ts new file mode 100644 index 0000000000000..e4bec88ba490f --- /dev/null +++ b/x-pack/test/security_api_integration/tests/audit/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('security APIs - Audit Log', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./audit_log')); + }); +} From 5da0cb3f909bbbb9c7140c11342ed1d21470b092 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 16 Oct 2020 15:45:19 -0400 Subject: [PATCH 72/79] [Ingest Manager] Fix for comparing versions with -SNAPSHOT suffix (#80742) * remove -SNAPSHOT from kibana version * add integration tests with -SNAPSHOT version of kibana * update isAgentUpgradeable to compare version numbers only * continue to send the kibana version with snapshot suffix to agent * cleanup code into one function * fix test to check for snapshot before adding suffix --- .../services/is_agent_upgradeable.test.ts | 31 ++++++++++++++ .../common/services/is_agent_upgradeable.ts | 11 +++-- .../server/routes/agent/upgrade_handler.ts | 32 +++++++++++---- .../apis/fleet/agents/upgrade.ts | 41 +++++++++++++++++++ 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts index ca0fcd3c52c9a..dc61f4898478d 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts @@ -135,4 +135,35 @@ describe('Ingest Manager - isAgentUpgradeable', () => { true ); }); + it('returns false if agent reports upgradeable, with agent snapshot version === kibana version', () => { + expect( + isAgentUpgradeable(getAgent({ version: '7.9.0-SNAPSHOT', upgradeable: true }), '7.9.0') + ).toBe(false); + }); + it('returns false if agent reports upgradeable, with agent version === kibana snapshot version', () => { + expect( + isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true }), '7.9.0-SNAPSHOT') + ).toBe(false); + }); + it('returns true if agent reports upgradeable, with agent snapshot version < kibana snapshot version', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0-SNAPSHOT', upgradeable: true }), + '8.0.0-SNAPSHOT' + ) + ).toBe(true); + }); + it('returns false if agent reports upgradeable, with agent snapshot version === kibana snapshot version', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '8.0.0-SNAPSHOT', upgradeable: true }), + '8.0.0-SNAPSHOT' + ) + ).toBe(false); + }); + it('returns true if agent reports upgradeable, with agent version < kibana snapshot version', () => { + expect( + isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0-SNAPSHOT') + ).toBe(true); + }); }); diff --git a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts index 7b59fb7b22825..b93e5d99543f6 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts @@ -14,9 +14,12 @@ export function isAgentUpgradeable(agent: Agent, kibanaVersion: string) { return false; } if (agent.unenrollment_started_at || agent.unenrolled_at) return false; - const kibanaVersionParsed = semver.parse(kibanaVersion); - const agentVersionParsed = semver.parse(agentVersion); - if (!agentVersionParsed || !kibanaVersionParsed) return false; if (!agent.local_metadata.elastic.agent.upgradeable) return false; - return semver.lt(agentVersionParsed, kibanaVersionParsed); + + // make sure versions are only the number before comparison + const agentVersionNumber = semver.coerce(agentVersion); + if (!agentVersionNumber) throw new Error('agent version is invalid'); + const kibanaVersionNumber = semver.coerce(kibanaVersion); + if (!kibanaVersionNumber) throw new Error('kibana version is invalid'); + return semver.lt(agentVersionNumber, kibanaVersionNumber); } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts index 9c6b50b6d8f09..60dc7c6ee5f2b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts @@ -6,6 +6,7 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; +import semver from 'semver'; import { AgentSOAttributes, PostAgentUpgradeResponse, @@ -26,17 +27,18 @@ export const postAgentUpgradeHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const { version, source_uri: sourceUri } = request.body; - - // temporarily only allow upgrading to the same version as the installed kibana version const kibanaVersion = appContextService.getKibanaVersion(); - if (kibanaVersion !== version) { + try { + checkVersionIsSame(version, kibanaVersion); + } catch (err) { return response.customError({ statusCode: 400, body: { - message: `cannot upgrade agent to ${version} because it is different than the installed kibana version ${kibanaVersion}`, + message: err.message, }, }); } + const agentSO = await soClient.get( AGENT_SAVED_OBJECT_TYPE, request.params.agentId @@ -82,14 +84,14 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const { version, source_uri: sourceUri, agents } = request.body; - - // temporarily only allow upgrading to the same version as the installed kibana version const kibanaVersion = appContextService.getKibanaVersion(); - if (kibanaVersion !== version) { + try { + checkVersionIsSame(version, kibanaVersion); + } catch (err) { return response.customError({ statusCode: 400, body: { - message: `cannot upgrade agent to ${version} because it is different than the installed kibana version ${kibanaVersion}`, + message: err.message, }, }); } @@ -115,3 +117,17 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< return defaultIngestErrorHandler({ error, response }); } }; + +export const checkVersionIsSame = (version: string, kibanaVersion: string) => { + // get version number only in case "-SNAPSHOT" is in it + const kibanaVersionNumber = semver.coerce(kibanaVersion)?.version; + if (!kibanaVersionNumber) throw new Error(`kibanaVersion ${kibanaVersionNumber} is not valid`); + const versionToUpgradeNumber = semver.coerce(version)?.version; + if (!versionToUpgradeNumber) + throw new Error(`version to upgrade ${versionToUpgradeNumber} is not valid`); + // temporarily only allow upgrading to the same version as the installed kibana version + if (kibanaVersionNumber !== versionToUpgradeNumber) + throw new Error( + `cannot upgrade agent to ${versionToUpgradeNumber} because it is different than the installed kibana version ${kibanaVersionNumber}` + ); +}; diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts index 055877c19c82f..c5426168eb78f 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts @@ -11,6 +11,10 @@ import { setupIngest } from './services'; import { skipIfNoDockerRegistry } from '../../../helpers'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../../../plugins/ingest_manager/common'; +const makeSnapshotVersion = (version: string) => { + return version.endsWith('-SNAPSHOT') ? version : `${version}-SNAPSHOT`; +}; + export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); @@ -48,6 +52,43 @@ export default function (providerContext: FtrProviderContext) { const res = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'); expect(typeof res.body.item.upgrade_started_at).to.be('string'); }); + it('should respond 400 if upgrading agent with version the same as snapshot version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: kibanaVersion } } }, + }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + }) + .expect(400); + }); + it('should respond 200 if upgrading agent with version less than kibana snapshot version', async () => { + const kibanaVersion = await kibanaServer.version.get(); + const kibanaVersionSnapshot = makeSnapshotVersion(kibanaVersion); + + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await supertest + .post(`/api/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersionSnapshot, + }) + .expect(200); + }); it('should respond 200 to upgrade agent and update the agent SO without source_uri', async () => { const kibanaVersion = await kibanaServer.version.get(); await kibanaServer.savedObjects.update({ From 7adfcd8f70fa5ce53bc2aa64bf64488c42f39a3c Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 16 Oct 2020 13:41:39 -0700 Subject: [PATCH 73/79] [keystore_cli] parse values as JSON before adding to keystore (#80848) Co-authored-by: spalger --- src/cli_keystore/add.js | 10 +++++++++- src/cli_keystore/add.test.js | 11 +++++++++++ src/legacy/server/keystore/keystore.test.js | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 232392f34c63b..d88256da1aa59 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -59,7 +59,15 @@ export async function add(keystore, key, options = {}) { value = await question(`Enter value for ${key}`, { mask: '*' }); } - keystore.add(key, value.trim()); + const parsedValue = value.trim(); + let parsedJsonValue; + try { + parsedJsonValue = JSON.parse(parsedValue); + } catch { + // noop, only treat value as json if it parses as JSON + } + + keystore.add(key, parsedJsonValue ?? parsedValue); keystore.save(); } diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index f1adee8879bc2..ba381ca2f3e14 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -129,6 +129,17 @@ describe('Kibana keystore', () => { expect(keystore.data.foo).toEqual('bar'); }); + it('parses JSON values', async () => { + prompt.question.returns(Promise.resolve('["bar"]\n')); + + const keystore = new Keystore('/data/test.keystore'); + sandbox.stub(keystore, 'save'); + + await add(keystore, 'foo'); + + expect(keystore.data.foo).toEqual(['bar']); + }); + it('persists updated keystore', async () => { prompt.question.returns(Promise.resolve('bar\n')); diff --git a/src/legacy/server/keystore/keystore.test.js b/src/legacy/server/keystore/keystore.test.js index 0897ce55d086b..e35edd1859484 100644 --- a/src/legacy/server/keystore/keystore.test.js +++ b/src/legacy/server/keystore/keystore.test.js @@ -157,11 +157,13 @@ describe('Keystore', () => { it('adds a key/value pair', () => { const keystore = new Keystore('/data/unprotected.keystore'); keystore.add('a3', 'baz'); + keystore.add('a4', [1, 'a', 2, 'b']); expect(keystore.data).toEqual({ 'a1.b2.c3': 'foo', a2: 'bar', a3: 'baz', + a4: [1, 'a', 2, 'b'], }); }); }); From 7e47e3935ea58cc69f566b8970f8f503e3fcbe36 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 16 Oct 2020 13:53:58 -0700 Subject: [PATCH 74/79] skip flaky suite (#80914) --- test/functional/apps/discover/_sidebar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.js index ce7ebff9cce74..f7784b739336b 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.js @@ -25,7 +25,8 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); - describe('discover sidebar', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/80914 + describe.skip('discover sidebar', function describeIndexTests() { before(async function () { // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ From 958d36e2f47539101c3416891e602b355a4a1a9f Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 16 Oct 2020 18:12:26 -0400 Subject: [PATCH 75/79] Added Enterprise Search config to kibana-docker (#80872) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../os_packages/docker_generator/resources/bin/kibana-docker | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 0039debe383bd..f5cf6c85fcbef 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -47,6 +47,10 @@ kibana_vars=( elasticsearch.ssl.truststore.password elasticsearch.ssl.verificationMode elasticsearch.username + enterpriseSearch.accessCheckTimeout + enterpriseSearch.accessCheckTimeoutWarning + enterpriseSearch.enabled + enterpriseSearch.host i18n.locale interpreter.enableInVisualize kibana.autocompleteTerminateAfter From 71f4c085b72034b1fc5e00c1c8914500da321f87 Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 16 Oct 2020 15:25:26 -0700 Subject: [PATCH 76/79] skip flaky suite (#80929) --- x-pack/test/accessibility/apps/home.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index 110201674b39a..280769bc09bc9 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const globalNav = getService('globalNav'); const testSubjects = getService('testSubjects'); - describe('Kibana Home', () => { + // FLAKY: https://github.com/elastic/kibana/issues/80929 + describe.skip('Kibana Home', () => { before(async () => { await PageObjects.common.navigateToApp('home'); }); From 72fa61ba71dee3b01da4c14db0aad3c9c13922b7 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Sat, 17 Oct 2020 00:48:56 +0100 Subject: [PATCH 77/79] Fix audit logger logging to console even when disabled (#80928) --- .../server/audit/audit_service.test.ts | 16 +---------- .../security/server/audit/audit_service.ts | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index 60dbe341dc4bf..e0dd98c7de639 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -74,20 +74,6 @@ describe('#setup', () => { expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable)); }); - it('does not configure logging when using legacy logger', async () => { - new AuditService(logger).setup({ - license, - config: { - enabled: true, - }, - logging, - http, - getCurrentUser, - getSpaceId, - }); - expect(logging.configure).not.toHaveBeenCalled(); - }); - it('registers post auth hook', () => { new AuditService(logger).setup({ license, @@ -181,7 +167,7 @@ describe('#createLoggingConfig', () => { "appenders": Array [ "auditTrailAppender", ], - "context": "audit", + "context": "audit.ecs", "level": "info", }, ], diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index b84ad37332b85..31c7e28be3b8c 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -67,7 +67,11 @@ export class AuditService { */ private allowAuditLogging = false; - constructor(private readonly logger: Logger) {} + private ecsLogger: Logger; + + constructor(private readonly logger: Logger) { + this.ecsLogger = logger.get('ecs'); + } setup({ license, @@ -83,16 +87,13 @@ export class AuditService { }); } - // Do not change logging for legacy logger - if (config.appender) { - // Configure logging during setup and when license changes - logging.configure( - license.features$.pipe( - distinctUntilKeyChanged('allowAuditLogging'), - createLoggingConfig(config) - ) - ); - } + // Configure logging during setup and when license changes + logging.configure( + license.features$.pipe( + distinctUntilKeyChanged('allowAuditLogging'), + createLoggingConfig(config) + ) + ); /** * Creates an {@link AuditLogger} scoped to the current request. @@ -146,7 +147,7 @@ export class AuditService { }, }; if (filterEvent(meta, config.ignore_filters)) { - this.logger.info(event.message!, meta); + this.ecsLogger.info(event.message!, meta); } }; return { log }; @@ -203,7 +204,7 @@ export const createLoggingConfig = (config: ConfigType['audit']) => }, loggers: [ { - context: 'audit', + context: 'audit.ecs', level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off', appenders: ['auditTrailAppender'], }, From ef2be2c7252c0105fa7392a57be9ace101067607 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Sat, 17 Oct 2020 10:48:58 +0300 Subject: [PATCH 78/79] server logs config paths to use for runner (#52980) * server logs config paths to use for runner * fix eslint issue * do not log config path for default config * update snapshots * fix other tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: spalger --- .../cli/start_servers/__snapshots__/args.test.js.snap | 10 ++++++++++ .../src/functional_tests/cli/start_servers/args.js | 4 +++- packages/kbn-test/src/functional_tests/tasks.js | 9 ++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap index 809b635369a39..cd3174d13c3e6 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/__snapshots__/args.test.js.snap @@ -26,6 +26,7 @@ Object { "debug": true, "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -35,6 +36,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -49,6 +51,7 @@ Object { "extraKbnOpts": Object { "server.foo": "bar", }, + "useDefaultConfig": true, } `; @@ -59,6 +62,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "quiet": true, + "useDefaultConfig": true, } `; @@ -69,6 +73,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "silent": true, + "useDefaultConfig": true, } `; @@ -78,6 +83,7 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -87,6 +93,7 @@ Object { "createLogger": [Function], "esFrom": "source", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; @@ -97,6 +104,7 @@ Object { "esFrom": "snapshot", "extraKbnOpts": undefined, "installDir": "foo", + "useDefaultConfig": true, } `; @@ -106,6 +114,7 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, "verbose": true, } `; @@ -116,5 +125,6 @@ Object { "createLogger": [Function], "esFrom": "snapshot", "extraKbnOpts": undefined, + "useDefaultConfig": true, } `; diff --git a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js index e604e86de8b3a..2b32726557ba3 100644 --- a/packages/kbn-test/src/functional_tests/cli/start_servers/args.js +++ b/packages/kbn-test/src/functional_tests/cli/start_servers/args.js @@ -75,7 +75,8 @@ export function displayHelp() { export function processOptions(userOptions, defaultConfigPath) { validateOptions(userOptions); - const config = userOptions.config || defaultConfigPath; + const useDefaultConfig = !userOptions.config; + const config = useDefaultConfig ? defaultConfigPath : userOptions.config; if (!config) { throw new Error(`functional_tests_server: config is required`); @@ -100,6 +101,7 @@ export function processOptions(userOptions, defaultConfigPath) { return { ...userOptions, config: resolve(config), + useDefaultConfig, createLogger, extraKbnOpts: userOptions._, }; diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 7d4fc84d47bda..c2833cbbda332 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -36,6 +36,13 @@ import { readConfigFile } from '../functional_test_runner/lib'; const makeSuccessMessage = (options) => { const installDirFlag = options.installDir ? ` --kibana-install-dir=${options.installDir}` : ''; + const configPaths = Array.isArray(options.config) ? options.config : [options.config]; + const pathsMessage = options.useDefaultConfig + ? '' + : configPaths + .map((path) => relative(process.cwd(), path)) + .map((path) => ` --config ${path}`) + .join(''); return ( '\n\n' + @@ -43,7 +50,7 @@ const makeSuccessMessage = (options) => { Elasticsearch and Kibana are ready for functional testing. Start the functional tests in another terminal session by running this command from this directory: - node ${relative(process.cwd(), KIBANA_FTR_SCRIPT)}${installDirFlag} + node ${relative(process.cwd(), KIBANA_FTR_SCRIPT)}${installDirFlag}${pathsMessage} ` + '\n\n' ); From 5858dd8f98153ea46f6350a5b04799541de65e1d Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Sat, 17 Oct 2020 12:25:31 +0200 Subject: [PATCH 79/79] [ML] Transforms: Fix tab ids for expanded row. (#80666) We based the IDs to identify expanded row tabs on the transform ID. This could break the page because the transform ID could include characters (e.g. dots) not supported by EUI's tabbed content component. This fixes the issue by using the stringHash() utility to create the IDs. --- .../transform_list/expanded_row.tsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index 4478edab0dba5..de45322d04987 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -26,6 +26,23 @@ function getItemDescription(value: any) { return value.toString(); } +/** + * Creates a deterministic number based hash out of a string. + */ +export function stringHash(str: string): number { + let hash = 0; + let chr = 0; + if (str.length === 0) { + return hash; + } + for (let i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise + hash |= 0; // eslint-disable-line no-bitwise + } + return hash < 0 ? hash * -2 : hash; +} + interface Item { title: string; description: any; @@ -162,9 +179,11 @@ export const ExpandedRow: FC = ({ item }) => { position: 'left', }; + const tabId = stringHash(item.id); + const tabs = [ { - id: `transform-details-tab-${item.id}`, + id: `transform-details-tab-${tabId}`, 'data-test-subj': 'transformDetailsTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformDetailsLabel', @@ -175,7 +194,7 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: `transform-stats-tab-${item.id}`, + id: `transform-stats-tab-${tabId}`, 'data-test-subj': 'transformStatsTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformStatsLabel', @@ -186,13 +205,13 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: `transform-json-tab-${item.id}`, + id: `transform-json-tab-${tabId}`, 'data-test-subj': 'transformJsonTab', name: 'JSON', content: , }, { - id: `transform-messages-tab-${item.id}`, + id: `transform-messages-tab-${tabId}`, 'data-test-subj': 'transformMessagesTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformMessagesLabel', @@ -203,7 +222,7 @@ export const ExpandedRow: FC = ({ item }) => { content: , }, { - id: `transform-preview-tab-${item.id}`, + id: `transform-preview-tab-${tabId}`, 'data-test-subj': 'transformPreviewTab', name: i18n.translate( 'xpack.transform.transformList.transformDetails.tabs.transformPreviewLabel',